<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:webfeeds="http://webfeeds.org/rss/1.0">
  <title>Mobile カテゴリ | フューチャー技術ブログ</title>
  <subtitle>Mobile カテゴリの記事一覧</subtitle>
  <icon>https://future-architect.github.io/feed_icon.png</icon>
  <logo>https://future-architect.github.io/apple-touch-icon.png</logo>
  <webfeeds:icon>https://future-architect.github.io/apple-touch-icon.png</webfeeds:icon>
  <webfeeds:accentColor>258fb8</webfeeds:accentColor>
  <link href="https://future-architect.github.io/categories/Mobile/atom.xml" rel="self"/>
  <link href="https://future-architect.github.io/categories/Mobile/"/>
  <updated>2025-10-21T15:00:00.000Z</updated>
  <id>https://future-architect.github.io/categories/Mobile/</id>
  <generator uri="https://hexo.io/">Hexo</generator>
  <entry>
    <title>Vue.jsでモバイルアプリ開発をするフレームワークたち(2025秋)</title>
    <link href="https://future-architect.github.io/articles/20251022a/"/>
    <id>https://future-architect.github.io/articles/20251022a/</id>
    <published>2025-10-21T15:00:00.000Z</published>
    <updated>2025-10-21T15:00:00.000Z</updated>
    <author><name>山本竜玄</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251022a/image.png" alt="" width="861" height="864">

<p>この記事は、Vue.js連載の4本目です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは。山本竜玄です。</p>
<p>モバイルアプリ開発と聞いて、どのような技術を思い浮かべるでしょうか。iOSならSwift、AndroidならKotlinといったネイティブ開発や、FlutterやReact Nativeといったクロスプラットフォームフレームワークが主流です。実際、2025年のクロスプラットフォーム市場ではFlutterが42%、React Nativeが38%のシェアを占めています<sup id="fnref:1">1</sup>。</p>
<p>一方で、Vue.jsでもモバイルアプリ開発ができることをご存知でしょうか。Webフロントエンド開発で人気のVue.jsですが、モバイルアプリ開発のフレームワークも複数存在します。既存のVue.jsスキルを活かしてモバイルアプリを開発できるため、Vue.jsの経験者が多いチームでは選択肢となりえます。</p>
<p>本記事では、2025年秋時点でのVue.jsモバイルアプリ開発フレームワークの状況を整理します。プロダクション環境で使える安定したフレームワークから、トレンド技術まで、調べた内容を紹介します。</p>
<h2 id="Vue-jsモバイル開発の全体像">Vue.jsモバイル開発の全体像</h2><p>クロスプラットフォーム開発市場は急成長しています。2025年の市場規模は124.5億ドルで、2032年には369.2億ドルに達する見込みです<sup id="fnref:2">2</sup>。</p>
<p>この市場の中で、Vue.jsはWebフロントエンド開発の人気フレームワークとして、モバイル開発にも選択肢を提供しています。FlutterやReact Nativeと比較すると、Vue.jsモバイルフレームワークの市場シェアは小さいものの、既存のWeb開発者チームでの有識者が多い場合には、モバイルアプリ開発への敷居が低いことは大きなメリットです。</p>
<p>本記事では、Vue.jsでモバイルアプリを開発できるフレームワークを調査し、以下のように紹介しています。</p>
<h3 id="主要フレームワーク（4つ）">主要フレームワーク（4つ）</h3><ul>
<li>Ionic Vue</li>
<li>Capacitor + Vue</li>
<li>NativeScript-Vue</li>
<li>Quasar Framework</li>
</ul>
<p>これらは継続的にメンテナンスされており、GitHubでも活発な開発が確認できます。</p>
<h3 id="中国市場向け（1つ）">中国市場向け（1つ）</h3><ul>
<li>uni-app</li>
</ul>
<p>中国のDCloudが開発しており、WeChatなどのミニプログラムとネイティブアプリを同時展開できます。中国国内では900万人の開発者が利用しているらしいです<sup id="fnref:4">3</sup>。</p>
<h3 id="新技術（1つ）">新技術（1つ）</h3><ul>
<li>Lynx + Vue</li>
</ul>
<p>2025年3月にByteDanceが発表したフレームワークです。TikTokやCapCutの技術スタックから生まれました。Vue統合はプロトタイプ段階ですが、今後の動向は注目したいです。</p>
<h3 id="メンテナンス終了（3つ）">メンテナンス終了（3つ）</h3><ul>
<li>Weex</li>
<li>Vue Native</li>
<li>Onsen UI</li>
</ul>
<p>これらは開発が停止してます。</p>
<h2 id="主要フレームワーク">主要フレームワーク</h2><p>主要な4つのフレームワークを紹介します。これらはいずれもVue 3に対応しており、GitHubで継続的に開発されています。</p>
<h3 id="Ionic-Vue">Ionic Vue</h3><p>Ionicは、Web技術でネイティブアプリを構築するフレームワークです。2013年にAngular向けにスタートし、現在はVue、React、Angularをサポートしています。</p>
<h4 id="基本情報">基本情報</h4><ul>
<li>最新バージョン: v8.7.6（2025年10月）</li>
<li>ライセンス: MIT</li>
<li>GitHub: 52,101スター（2025年10月時点）<sup id="fnref:5">4</sup></li>
<li>npm: 週間18,911ダウンロード<sup id="fnref:6">5</sup></li>
<li>公式サイト: https://ionicframework.com/docs/vue/overview</li>
</ul>
<h4 id="特徴">特徴</h4><ul>
<li>iOS、Android、Webの3プラットフォーム対応</li>
<li>Capacitorによるネイティブ機能アクセス</li>
<li>Vue 3とComposition API対応</li>
<li>Material DesignとiOSデザインの両対応</li>
<li>UIコンポーネントライブラリを提供</li>
</ul>
<h4 id="プロジェクト作成">プロジェクト作成</h4><p>Ionic CLIを使用することで、すぐに開発を開始できます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install -g @ionic/cli</span><br><span class="line">ionic start myApp tabs --<span class="built_in">type</span> vue</span><br><span class="line"><span class="built_in">cd</span> myApp</span><br><span class="line">ionic serve</span><br></pre></td></tr></table></figure>

<h4 id="Vue-jsとの違い">Vue.jsとの違い</h4><p>Ionic Vueは、通常のVue.jsプロジェクトにモバイルアプリ特有の機能を追加します。主な違いは以下の3点です。</p>
<h5 id="1-IonicVueプラグインの追加（main-ts）">1. IonicVueプラグインの追加（main.ts）</h5><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createApp &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">IonicVue</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ionic/vue&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">App</span> <span class="keyword">from</span> <span class="string">&#x27;./App.vue&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> router <span class="keyword">from</span> <span class="string">&#x27;./router&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Core CSS required for Ionic components to work properly */</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;@ionic/vue/css/core.css&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Basic CSS for apps built with Ionic */</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;@ionic/vue/css/normalize.css&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;@ionic/vue/css/structure.css&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;@ionic/vue/css/typography.css&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Theme variables */</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;./theme/variables.css&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> app = <span class="title function_">createApp</span>(<span class="title class_">App</span>)</span><br><span class="line">  .<span class="title function_">use</span>(<span class="title class_">IonicVue</span>)</span><br><span class="line">  .<span class="title function_">use</span>(router);</span><br><span class="line"></span><br><span class="line">router.<span class="title function_">isReady</span>().<span class="title function_">then</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  app.<span class="title function_">mount</span>(<span class="string">&#x27;#app&#x27;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<h5 id="2-IonRouterOutletの使用（App-vue）">2. IonRouterOutletの使用（App.vue）</h5><p><code>IonRouterOutlet</code>は、複数のページを同じアウトレット内にレンダリングし、ページ遷移アニメーションとライフサイクルを管理します。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">ion-app</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">ion-router-outlet</span> /&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">ion-app</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; <span class="title class_">IonApp</span>, <span class="title class_">IonRouterOutlet</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ionic/vue&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h5 id="3-ionic-vue-routerの使用（router-index-ts）">3. @ionic&#x2F;vue-routerの使用（router&#x2F;index.ts）</h5><p>Vue Routerをベースにしつつ、モバイルアプリ特有のページ遷移アニメーションとナビゲーションスタックを実現します。タブは個別のナビゲーションスタックとして扱われます。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createRouter, createWebHistory &#125; <span class="keyword">from</span> <span class="string">&#x27;@ionic/vue-router&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">RouteRecordRaw</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;vue-router&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">HomePage</span> <span class="keyword">from</span> <span class="string">&#x27;../views/HomePage.vue&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">routes</span>: <span class="title class_">Array</span>&lt;<span class="title class_">RouteRecordRaw</span>&gt; = [</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">path</span>: <span class="string">&#x27;/&#x27;</span>,</span><br><span class="line">    <span class="attr">redirect</span>: <span class="string">&#x27;/home&#x27;</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">path</span>: <span class="string">&#x27;/home&#x27;</span>,</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;Home&#x27;</span>,</span><br><span class="line">    <span class="attr">component</span>: <span class="title class_">HomePage</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">];</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> router = <span class="title function_">createRouter</span>(&#123;</span><br><span class="line">  <span class="attr">history</span>: <span class="title function_">createWebHistory</span>(<span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">env</span>.<span class="property">BASE_URL</span>),</span><br><span class="line">  routes,</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> router;</span><br></pre></td></tr></table></figure>

<h4 id="ページコンポーネントの例">ページコンポーネントの例</h4><p>ページコンポーネントは<code>IonPage</code>をルート要素として使用します。以下はQuickstartで生成される<code>HomePage.vue</code>の例です。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">ion-page</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">ion-header</span> <span class="attr">:translucent</span>=<span class="string">&quot;true&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">ion-toolbar</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">ion-title</span>&gt;</span>Blank<span class="tag">&lt;/<span class="name">ion-title</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">ion-toolbar</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">ion-header</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">ion-content</span> <span class="attr">:fullscreen</span>=<span class="string">&quot;true&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">ion-header</span> <span class="attr">collapse</span>=<span class="string">&quot;condense&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">ion-toolbar</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">ion-title</span> <span class="attr">size</span>=<span class="string">&quot;large&quot;</span>&gt;</span>Blank<span class="tag">&lt;/<span class="name">ion-title</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">ion-toolbar</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">ion-header</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;container&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">strong</span>&gt;</span>Ready to create an app?<span class="tag">&lt;/<span class="name">strong</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">p</span>&gt;</span></span><br><span class="line">          Start with Ionic</span><br><span class="line">          <span class="tag">&lt;<span class="name">a</span> <span class="attr">target</span>=<span class="string">&quot;_blank&quot;</span> <span class="attr">rel</span>=<span class="string">&quot;noopener noreferrer&quot;</span> <span class="attr">href</span>=<span class="string">&quot;https://ionicframework.com/docs/components&quot;</span></span></span><br><span class="line"><span class="tag">            &gt;</span>UI Components&lt;/a</span><br><span class="line">          &gt;</span><br><span class="line">        <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">ion-content</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">ion-page</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; <span class="title class_">IonContent</span>, <span class="title class_">IonHeader</span>, <span class="title class_">IonPage</span>, <span class="title class_">IonTitle</span>, <span class="title class_">IonToolbar</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@ionic/vue&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><code>collapse=&quot;condense&quot;</code>を使用すると、スクロール時に大きなタイトルが小さく折りたたまれるモバイル特有のUIを実現できます。</p>
<p>参考</p>
<ul>
<li>Ionic Vue Quickstart</li>
<li>Ionic Vue Navigation</li>
<li>Ionic Vue Overview</li>
</ul>
<h3 id="Capacitor-Vue">Capacitor + Vue</h3><p>Capacitorは、既存のWebアプリをモバイルアプリに変換するためのランタイムです。Ionic VueもCapacitorを使用していますが、Ionic Vueが専用UIコンポーネントライブラリとセットになっているのに対し、Capacitor単体では既存のVue.jsアプリをそのままモバイル化できる点が特徴です。</p>
<h4 id="基本情報-1">基本情報</h4><ul>
<li>最新バージョン: v7.4.3（2025年10月）</li>
<li>ライセンス: MIT</li>
<li>GitHub: 14,200スター（2025年10月時点）<sup id="fnref:7">6</sup></li>
<li>npm: 週間800,692ダウンロード<sup id="fnref:8">7</sup></li>
<li>公式サイト: https://capacitorjs.com/</li>
</ul>
<h4 id="特徴-1">特徴</h4><ul>
<li>iOS、Android、Web、PWA対応</li>
<li>既存のVue.jsアプリをそのままモバイル化可能</li>
<li>35以上の公式プラグイン（Camera、Geolocation、Filesystemなど）</li>
<li>Vite、Vue CLI、Nuxtなど主要ビルドツールに対応</li>
<li>ネイティブコードとの共存が可能</li>
</ul>
<h4 id="プロジェクトへの追加">プロジェクトへの追加</h4><p>既存のVue.jsプロジェクトにCapacitorを追加する手順です。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install @capacitor/core @capacitor/cli</span><br><span class="line">npx <span class="built_in">cap</span> init [name] [<span class="built_in">id</span>] --web-dir=dist</span><br></pre></td></tr></table></figure>

<p>ビルド後、プラットフォームを追加します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm run build</span><br><span class="line">npm i @capacitor/ios @capacitor/android</span><br><span class="line">npx <span class="built_in">cap</span> add android</span><br><span class="line">npx <span class="built_in">cap</span> add ios</span><br></pre></td></tr></table></figure>

<h4 id="Vue-jsでのネイティブAPI呼び出し">Vue.jsでのネイティブAPI呼び出し</h4><p>CapacitorのAPIは、Vue.jsのコンポーネント内でJavaScriptの標準的な非同期関数として呼び出せます。以下はGeolocation APIの例です。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Geolocation</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@capacitor/geolocation&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getCurrentPosition</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> position = <span class="keyword">await</span> <span class="title class_">Geolocation</span>.<span class="title function_">getCurrentPosition</span>();</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Current position:&#x27;</span>, position);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>Camera APIを使った実装例です。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; ref &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; <span class="title class_">Camera</span>, <span class="title class_">CameraResultType</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@capacitor/camera&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> imageUrl = <span class="title function_">ref</span>(<span class="string">&#x27;&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> <span class="title function_">capturePhoto</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">const</span> photo = <span class="keyword">await</span> <span class="title class_">Camera</span>.<span class="title function_">getPhoto</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">quality</span>: <span class="number">90</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">allowEditing</span>: <span class="literal">true</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">resultType</span>: <span class="title class_">CameraResultType</span>.<span class="property">Uri</span></span></span><br><span class="line"><span class="language-javascript">  &#125;);</span></span><br><span class="line"><span class="language-javascript">  imageUrl.<span class="property">value</span> = photo.<span class="property">webPath</span> || <span class="string">&#x27;&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript">&#125;;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">button</span> @<span class="attr">click</span>=<span class="string">&quot;capturePhoto&quot;</span>&gt;</span>写真を撮影<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">img</span> <span class="attr">v-if</span>=<span class="string">&quot;imageUrl&quot;</span> <span class="attr">:src</span>=<span class="string">&quot;imageUrl&quot;</span> <span class="attr">alt</span>=<span class="string">&quot;撮影した写真&quot;</span> /&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>参考</p>
<ul>
<li>Capacitor Documentation</li>
<li>Using Capacitor with Vue</li>
<li>Capacitor APIs</li>
</ul>
<h3 id="NativeScript-Vue">NativeScript-Vue</h3><p>NativeScript-Vueは、WebViewを使用せず、真のネイティブUIコンポーネントでアプリを構築するフレームワークです。Vueのテンプレートは、iOSでは<code>UILabel</code>、Androidでは<code>android.widget.TextView</code>といったネイティブコンポーネントに直接レンダリングされます。</p>
<h4 id="基本情報-2">基本情報</h4><ul>
<li>最新バージョン: v3.0.2（2025年10月17日）</li>
<li>ライセンス: MIT</li>
<li>GitHub: 5,900スター（2025年10月時点）<sup id="fnref:9">8</sup></li>
<li>npm: 週間2,338ダウンロード<sup id="fnref:10">9</sup></li>
<li>公式サイト: https://nativescript-vue.org/</li>
</ul>
<h4 id="特徴-2">特徴</h4><ul>
<li>iOS、Android対応</li>
<li>WebViewを使用しない真のネイティブUI</li>
<li>Vue 3完全対応（Options API、Composition API両対応）</li>
<li>JavaScriptから全てのネイティブAPIに直接アクセス可能</li>
<li>V8エンジンで実行される埋め込みJavaScript環境</li>
<li>TypeScript対応の改善とモダンなプラグインシステム</li>
</ul>
<h4 id="プロジェクト作成-1">プロジェクト作成</h4><p>NativeScript CLIを使用してプロジェクトを作成します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install -g nativescript</span><br><span class="line">ns create myAwesomeApp --vue</span><br><span class="line"><span class="built_in">cd</span> myAwesomeApp</span><br><span class="line">ns run ios</span><br><span class="line"><span class="comment"># または</span></span><br><span class="line">ns run android</span><br></pre></td></tr></table></figure>

<blockquote>
<p><strong>注</strong>: 2025年10月時点では<code>--template @nativescript-vue/template-blank@latest</code>の使用時にバージョン8.0.0が見つからない場合があります。その場合は<code>--vue</code>フラグを使用してください。</p>
</blockquote>
<h4 id="ネイティブUIコンポーネントの使用">ネイティブUIコンポーネントの使用</h4><p>NativeScript-Vueでは、Vueのテンプレート内でネイティブUIコンポーネントを直接使用します。以下はリスト表示の例です。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">Page</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">ActionBar</span> <span class="attr">title</span>=<span class="string">&quot;NativeFlix&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">ListView</span> <span class="attr">height</span>=<span class="string">&quot;100%&quot;</span> <span class="attr">separatorColor</span>=<span class="string">&quot;transparent&quot;</span> <span class="attr">for</span>=<span class="string">&quot;item in flicks&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">v-template</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">GridLayout</span> <span class="attr">height</span>=<span class="string">&quot;280&quot;</span> <span class="attr">rows</span>=<span class="string">&quot;*, auto, auto&quot;</span> <span class="attr">columns</span>=<span class="string">&quot;*&quot;</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">Image</span> <span class="attr">row</span>=<span class="string">&quot;0&quot;</span> <span class="attr">:src</span>=<span class="string">&quot;item.image&quot;</span> /&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">Label</span> <span class="attr">row</span>=<span class="string">&quot;1&quot;</span> <span class="attr">:text</span>=<span class="string">&quot;item.title&quot;</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">GridLayout</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">v-template</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">ListView</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">Page</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="title function_">data</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">return</span> &#123;</span></span><br><span class="line"><span class="language-javascript">      <span class="attr">flicks</span>: [</span></span><br><span class="line"><span class="language-javascript">        &#123; <span class="attr">id</span>: <span class="number">1</span>, <span class="attr">title</span>: <span class="string">&#x27;Movie 1&#x27;</span>, <span class="attr">image</span>: <span class="string">&#x27;~/assets/movie1.jpg&#x27;</span> &#125;,</span></span><br><span class="line"><span class="language-javascript">        &#123; <span class="attr">id</span>: <span class="number">2</span>, <span class="attr">title</span>: <span class="string">&#x27;Movie 2&#x27;</span>, <span class="attr">image</span>: <span class="string">&#x27;~/assets/movie2.jpg&#x27;</span> &#125;,</span></span><br><span class="line"><span class="language-javascript">      ]</span></span><br><span class="line"><span class="language-javascript">    &#125;;</span></span><br><span class="line"><span class="language-javascript">  &#125;</span></span><br><span class="line"><span class="language-javascript">&#125;;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>ここで使用されているコンポーネントは以下の通りです。</p>
<ul>
<li><code>ActionBar</code>: ページヘッダー</li>
<li><code>ListView</code>: スクロール可能なリスト</li>
<li><code>GridLayout</code>: グリッドレイアウトコンテナ</li>
<li><code>Image</code>: 画像表示</li>
<li><code>Label</code>: テキストラベル</li>
</ul>
<p>これらは全てネイティブUIコンポーネントとしてレンダリングされ、WebViewのオーバーヘッドがありません。</p>
<p>参考</p>
<ul>
<li>NativeScript-Vue Getting Started</li>
<li>Build a master-detail app with Vue</li>
</ul>
<h3 id="Quasar-Framework">Quasar Framework</h3><p>Quasarは、単一コードベースで複数プラットフォームに展開できるVue.jsフレームワークです。Web、モバイル、デスクトップアプリを同時に開発できる点が特徴で、エンタープライズ向けのフルスタックソリューションを提供します。</p>
<h4 id="基本情報-3">基本情報</h4><ul>
<li>最新バージョン: v2.18.5（2025年10月）</li>
<li>ライセンス: MIT</li>
<li>GitHub: 26,800スター（2025年10月時点）<sup id="fnref:11">10</sup></li>
<li>npm: 週間156,502ダウンロード<sup id="fnref:12">11</sup></li>
<li>公式サイト: https://quasar.dev/</li>
</ul>
<h4 id="特徴-3">特徴</h4><ul>
<li>Web、iOS、Android、デスクトップ（Electron）、ブラウザ拡張機能対応</li>
<li>70以上のMaterial Design準拠のUIコンポーネント</li>
<li>SSR、PWA、SPAに対応</li>
<li>Capacitor&#x2F;Cordovaによるモバイルアプリ化</li>
<li>Vue 3とComposition API完全対応</li>
<li>Viteベースの高速開発環境</li>
<li>CLIツールによる統合開発体験</li>
</ul>
<h4 id="プロジェクト作成-2">プロジェクト作成</h4><p>Quasar CLIを使用することで、対話的にプロジェクトを作成できます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm create quasar</span><br><span class="line"><span class="comment"># または</span></span><br><span class="line">yarn create quasar</span><br></pre></td></tr></table></figure>

<p>対話形式で以下を選択します。</p>
<ul>
<li>App with Quasar CLI</li>
<li>Quasar v2</li>
<li>Vite（推奨）またはWebpack</li>
</ul>
<p>グローバルCLIをインストールすると、コマンドを直接実行できます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install -g @quasar/cli</span><br><span class="line">quasar dev</span><br><span class="line">quasar build</span><br></pre></td></tr></table></figure>

<blockquote>
<p><strong>補足</strong>: 対話形式を避けたい場合や、既存のVue 3プロジェクトにQuasarを追加する場合は、以下の手動セットアップも可能です。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Vue 3プロジェクトを作成</span></span><br><span class="line">npm create vue@latest my-app</span><br><span class="line"><span class="built_in">cd</span> my-app</span><br><span class="line">npm install</span><br><span class="line"></span><br><span class="line"><span class="comment"># Quasarをインストール</span></span><br><span class="line">npm install quasar @quasar/extras</span><br><span class="line">npm install -D @quasar/vite-plugin sass</span><br><span class="line"></span><br><span class="line"><span class="comment"># vite.config.tsとmain.tsを設定（詳細は公式ドキュメント参照）</span></span><br></pre></td></tr></table></figure>
</blockquote>
<h4 id="モバイルアプリ化">モバイルアプリ化</h4><p>QuasarはCapacitorまたはCordovaを使用してモバイルアプリに変換します。Capacitorを使用する例です。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">quasar mode add capacitor</span><br><span class="line">quasar dev -m capacitor -T android</span><br><span class="line">quasar dev -m capacitor -T ios</span><br></pre></td></tr></table></figure>

<p>Capacitorモードでは、Webアプリとして開発したコードがそのままモバイルアプリになります。</p>
<h4 id="コンポーネントの使用例">コンポーネントの使用例</h4><p>Quasarのコンポーネントは、自動インポートに対応しているため、設定なしで使用できます。以下はカードとボタンを使用した例です。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">q-page</span> <span class="attr">class</span>=<span class="string">&quot;flex flex-center&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">q-card</span> <span class="attr">class</span>=<span class="string">&quot;q-pa-md&quot;</span> <span class="attr">style</span>=<span class="string">&quot;min-width: 350px&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">q-card-section</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;text-h6&quot;</span>&gt;</span>Quasar Framework<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;text-subtitle2&quot;</span>&gt;</span>モバイルアプリ開発<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">q-card-section</span>&gt;</span></span><br><span class="line"></span><br><span class="line">      <span class="tag">&lt;<span class="name">q-card-section</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">q-input</span></span></span><br><span class="line"><span class="tag">          <span class="attr">v-model</span>=<span class="string">&quot;text&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">label</span>=<span class="string">&quot;テキスト入力&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">outlined</span></span></span><br><span class="line"><span class="tag">        /&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">q-card-section</span>&gt;</span></span><br><span class="line"></span><br><span class="line">      <span class="tag">&lt;<span class="name">q-card-actions</span> <span class="attr">align</span>=<span class="string">&quot;right&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">q-btn</span></span></span><br><span class="line"><span class="tag">          <span class="attr">label</span>=<span class="string">&quot;送信&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">color</span>=<span class="string">&quot;primary&quot;</span></span></span><br><span class="line"><span class="tag">          @<span class="attr">click</span>=<span class="string">&quot;onSubmit&quot;</span></span></span><br><span class="line"><span class="tag">        /&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">q-card-actions</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">q-card</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">q-page</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; ref &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> text = <span class="title function_">ref</span>(<span class="string">&#x27;&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> <span class="title function_">onSubmit</span> = (<span class="params"></span>) =&gt; &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;送信:&#x27;</span>, text.<span class="property">value</span>);</span></span><br><span class="line"><span class="language-javascript">&#125;;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h4 id="レイアウトシステム">レイアウトシステム</h4><p>QuasarはFlexboxベースの12ポイントグリッドシステムを提供します。レスポンシブデザインに対応し、画面サイズに応じてレイアウトを自動調整できます。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;row&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;col-12 col-sm-6 col-md-4&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">q-card</span>&gt;</span>カード1<span class="tag">&lt;/<span class="name">q-card</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;col-12 col-sm-6 col-md-4&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">q-card</span>&gt;</span>カード2<span class="tag">&lt;/<span class="name">q-card</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;col-12 col-sm-6 col-md-4&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">q-card</span>&gt;</span>カード3<span class="tag">&lt;/<span class="name">q-card</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>このコードは、小さい画面では縦に並び、中サイズ画面では2列、大きい画面では3列で表示されます。</p>
<p>参考:</p>
<ul>
<li>Quasar Framework</li>
<li>Quasar CLI</li>
<li>Developing Capacitor Apps</li>
<li>Flex Grid</li>
</ul>
<h2 id="uni-app-中国市場向けフレームワーク">uni-app - 中国市場向けフレームワーク</h2><h3 id="uni-app">uni-app</h3><p>uni-appは、中国のDCloudが開発するクロスプラットフォームフレームワークです。ネイティブアプリに加えて、WeChatやAlipayなどのミニプログラム（小程序）に対応している点が最大の特徴です。</p>
<h4 id="基本情報-4">基本情報</h4><ul>
<li>開発元: DCloud</li>
<li>ライセンス: Apache 2.0</li>
<li>GitHub: 40,300スター（2025年1月時点）<sup id="fnref:13">12</sup></li>
<li>開発者数: 900万人以上</li>
<li>月間ユーザー数: 12億人のモバイルユーザー<sup id="fnref:14">13</sup></li>
<li>公式サイト: https://en.uniapp.dcloud.io/</li>
</ul>
<h4 id="特徴-4">特徴</h4><ul>
<li>iOS、Android、HarmonyOS Next対応</li>
<li>Web（レスポンシブ）対応</li>
<li>ミニプログラム対応（WeChat、Alipay、Baidu、Douyin、Larkなど）</li>
<li>Vue.js構文ベース</li>
<li>WeChatミニプログラムAPIとの互換性</li>
<li>条件付きコンパイルによるプラットフォーム別対応</li>
<li>ネイティブレンダリング対応</li>
</ul>
<h4 id="なぜ中国で人気なのか">なぜ中国で人気なのか</h4><p>中国市場では、WeChatやAlipayなどのスーパーアプリ内で動作するミニプログラムが普及しています。uni-appは、ミニプログラムとネイティブアプリを単一コードベースで開発できるため、中国市場向けアプリ開発で広く採用されているようです。</p>
<p>参考</p>
<ul>
<li>uni-app Official Website</li>
<li>uni-app History</li>
</ul>
<h2 id="Lynx-Vue">Lynx + Vue</h2><h3 id="Lynx">Lynx</h3><p>Lynxは、ByteDanceが2025年3月5日にオープンソース化したクロスプラットフォームフレームワークです。TikTokのSearch、Shop、Live機能や、TikTok Studioアプリの開発に使用されている実績があり、ByteDanceの技術スタックから生まれた注目のフレームワークです。</p>
<h4 id="基本情報-5">基本情報</h4><ul>
<li>開発元: ByteDance</li>
<li>発表日: 2025年3月5日</li>
<li>GitHub: 13,300スター（2025年10月時点）<sup id="fnref:15">14</sup></li>
<li>最新バージョン: v3.4.2（2025年10月9日）</li>
<li>コミット数: 2,104件</li>
<li>貢献者: 82人</li>
<li>公式サイト: https://lynxjs.org/</li>
</ul>
<h4 id="特徴-5">特徴</h4><ul>
<li>iOS、Android、Web対応（OpenHarmony、macOS、Windowsは開発中）</li>
<li>Rust製の高速レンダリングエンジン</li>
<li>デュアルスレッドアーキテクチャによる高性能</li>
<li>フレームワーク非依存設計（React、Vue、Svelteなどに対応）</li>
<li>PrimJS（QuickJSベース）によるJavaScript実行環境</li>
<li>初回フレーム描画の高速化（2〜4倍高速と報告）</li>
</ul>
<h4 id="Vue統合の状況">Vue統合の状況</h4><p>Lynxはフレームワーク非依存の設計で、現在はReactLynxが公式に提供されています。ByteDance社内では、Lynxの約半分のプロジェクトでReact以外のフレームワーク（VueやSvelte）が使用されています<sup id="fnref:16">15</sup>。</p>
<p>Vue.jsの創設者であるEvan Youは、Xでの投稿で以下のように述べ、Vue統合への支援を表明しています<sup id="fnref:17">16</sup>。</p>
<p>https://x.com/youyuxi/status/1898663514581168173</p>
<p>Lynx発表から、VueコミュニティのメンバーであるRahul Vashishthaがプロトタイプ開発を開始しました。2025年10月時点では、Vue統合はまだプロトタイプ段階であり、正式なリリースには至っていません。</p>
<h4 id="2025年のロードマップ">2025年のロードマップ</h4><p>Lynxチームは2025年に5つの安定版リリースを計画しています<sup id="fnref:18">17</sup>。</p>
<ul>
<li>v3.2（2025年4月）: リリース済み</li>
<li>v3.3（2025年6月）: リリース済み</li>
<li>v3.4（2025年10月）: リリース済み、OpenHarmonyサポート</li>
<li>v3.5（2025年10月）: macOS、Windowsサポート</li>
<li>v3.6（2025年12月）: 予定</li>
</ul>
<p>機能面では、input要素、ViewPager、Swiper、SVGサポート、Canvas API、Lottieアニメーション、通知、位置情報などの追加が計画されています。</p>
<h4 id="現状の制約">現状の制約</h4><p>2025年10月時点で、Lynxには以下の制約があります。</p>
<ul>
<li>エコシステムがまだ未成熟</li>
<li>サードパーティライブラリの不足</li>
<li>ドキュメントとチュートリアルが限定的</li>
<li>標準HTML要素（button、input）非対応（代わりに<code>&lt;view&gt;</code>, <code>&lt;text&gt;</code>, <code>&lt;image&gt;</code>などの独自要素を使用）</li>
<li>既存ネイティブアプリへの統合が前提（スタンドアロンビルド非対応）</li>
</ul>
<p>エコシステムがまだ未成熟であり、開発チームは既存アプリへの段階的な統合を推奨しています。新規アプリをゼロから開発する用途には、エコシステムの成熟を待つことが推奨されます<sup id="fnref:19">18</sup>。</p>
<p>ByteDanceの技術力と投資規模を考えると、Vue.jsとモバイルアプリの目線でもLynxの将来性は注目に値します。</p>
<h2 id="メンテナンス終了したフレームワーク">メンテナンス終了したフレームワーク</h2><p>かつて使われていたフレームワークですが、現在は開発が停止しています。新規プロジェクトでの採用は推奨しません。</p>
<h3 id="Weex（開発終了）">Weex（開発終了）</h3><p>Weexは、Alibabaが開発したクロスプラットフォームフレームワークで、VueとRaxの構文でモバイルアプリを構築できました。2016年にオープンソース化され、2017年からApache Incubatorプロジェクトとして開発されていましたが、2021年5月にApacheから引退し、開発が事実上停止しています<sup id="fnref:21">19</sup>。</p>
<ul>
<li>GitHub: 13,800スター（アーカイブ化）</li>
<li>最終コミット: 4年前（2021年頃）</li>
<li>現状: Apache incubator-weexリポジトリはアーカイブ化</li>
</ul>
<h3 id="Vue-Native（非推奨）">Vue Native（非推奨）</h3><p>Vue Nativeは、React Native上でVue.js構文を使用できるようにしたフレームワークです。Vue.jsのコードをReact Nativeコードに変換する仕組みでしたが、2021年11月に正式に非推奨化されました<sup id="fnref:22">20</sup>。</p>
<ul>
<li>開発元: GeekyAnts</li>
<li>非推奨化: 2021年11月</li>
<li>理由: メンテナンス継続が困難</li>
</ul>
<h3 id="Onsen-UI（限定的メンテナンス）">Onsen UI（限定的メンテナンス）</h3><p>Onsen UIは、PWAとハイブリッドアプリ向けのUIフレームワークで、Vue、React、Angularのバインディングを提供しています。Web Componentsベースで設計されており、Cordovaと組み合わせて使用します<sup id="fnref:23">21</sup>。</p>
<ul>
<li>GitHub: OnsenUI&#x2F;OnsenUI（最終コミット約1年前）</li>
<li>Vue 3対応: 公式ドキュメントあり</li>
<li>現状: 限定的なメンテナンス</li>
</ul>
<p>メインリポジトリは2023〜2024年頃まで更新がありましたが、活発な開発は行われていません。</p>
<h2 id="まとめ">まとめ</h2><p>本記事では、2025年秋時点でのVue.jsモバイルアプリ開発フレームワークの状況を調べて整理しました。</p>
<p>Vue.jsでのモバイルアプリ開発には、想像以上に豊富な選択肢があります。Ionic Vue、Capacitor、NativeScript-Vue、Quasarの4つの主要フレームワークは、いずれも継続的にメンテナンスされており、プロダクション環境での利用に適しています。中国市場向けにはuni-app、最近のトレンドや将来性としてはLynxという選択肢も存在します。</p>
<p>FlutterやReact Nativeと比較すると市場シェアは小さいものの、既存のVue.jsチームがモバイルアプリ開発に参入する際の敷居は低く、学習コストを抑えられる点が大きなメリットです。</p>
<p>2024〜2025年のVue.jsエコシステムの進化（Vue 3.5のメモリ削減とパフォーマンス改善、Vite 6.0のリリース、Nuxt 4の正式版）は、モバイルアプリ開発にも好影響を与えています。特にVue 3.5のメモリ使用量56%削減と配列操作の高速化は、リソース制約のあるモバイル環境で効果を発揮します。</p>
<p>Vue.jsモバイルアプリ開発の選択肢は今後も拡大していくでしょう。特にLynxのような新技術や、PWA&#x2F;ハイブリッドアプローチの進化に注目です。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">Flutter vs React Native: Full Comparison for Developers in 2025</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">Cross-Platform App Development Framework Market Size &amp; Share, 2032</span> ↩</li><li id="fn:4"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;">uni-app - Framework</span> ↩</li><li id="fn:5"><span style="vertical-align: top; padding-right: 10px;">4.</span><span style="vertical-align: top;">ionic-team/ionic-framework - GitHub</span> ↩</li><li id="fn:6"><span style="vertical-align: top; padding-right: 10px;">5.</span><span style="vertical-align: top;">@ionic/vue - npm Package Health Analysis | Snyk</span> ↩</li><li id="fn:7"><span style="vertical-align: top; padding-right: 10px;">6.</span><span style="vertical-align: top;">ionic-team/capacitor - GitHub</span> ↩</li><li id="fn:8"><span style="vertical-align: top; padding-right: 10px;">7.</span><span style="vertical-align: top;">npm downloads for @capacitor/core</span> ↩</li><li id="fn:9"><span style="vertical-align: top; padding-right: 10px;">8.</span><span style="vertical-align: top;">nativescript-vue/nativescript-vue - GitHub</span> ↩</li><li id="fn:10"><span style="vertical-align: top; padding-right: 10px;">9.</span><span style="vertical-align: top;">npm downloads for nativescript-vue</span> ↩</li><li id="fn:11"><span style="vertical-align: top; padding-right: 10px;">10.</span><span style="vertical-align: top;">quasarframework/quasar - GitHub</span> ↩</li><li id="fn:12"><span style="vertical-align: top; padding-right: 10px;">11.</span><span style="vertical-align: top;">quasar - npm trends</span> ↩</li><li id="fn:13"><span style="vertical-align: top; padding-right: 10px;">12.</span><span style="vertical-align: top;">dcloudio/uni-app - GitHub</span> ↩</li><li id="fn:14"><span style="vertical-align: top; padding-right: 10px;">13.</span><span style="vertical-align: top;">uni-app Official Website</span> ↩</li><li id="fn:15"><span style="vertical-align: top; padding-right: 10px;">14.</span><span style="vertical-align: top;">lynx-family/lynx - GitHub</span> ↩</li><li id="fn:16"><span style="vertical-align: top; padding-right: 10px;">15.</span><span style="vertical-align: top;">TikTok's Native Cross-Platform UI Framework Lynx Goes Open Source - InfoQ</span> ↩</li><li id="fn:17"><span style="vertical-align: top; padding-right: 10px;">16.</span><span style="vertical-align: top;">Vue Native? Vue + Lynx | Vue Mastery</span> ↩</li><li id="fn:18"><span style="vertical-align: top; padding-right: 10px;">17.</span><span style="vertical-align: top;">Lynx Roadmap 2025</span> ↩</li><li id="fn:19"><span style="vertical-align: top; padding-right: 10px;">18.</span><span style="vertical-align: top;">Lynx: Unlock Native for More</span> ↩</li><li id="fn:21"><span style="vertical-align: top; padding-right: 10px;">19.</span><span style="vertical-align: top;">apache/incubator-weex - GitHub</span> ↩</li><li id="fn:22"><span style="vertical-align: top; padding-right: 10px;">20.</span><span style="vertical-align: top;">Deprecation notice - GeekyAnts/vue-native-core</span> ↩</li><li id="fn:23"><span style="vertical-align: top; padding-right: 10px;">21.</span><span style="vertical-align: top;">Onsen UI - Vue components</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">2025年秋時点でのVue.jsモバイルアプリ開発フレームワークの状況を整理します。プロダクション環境で使える安定したフレームワークから、トレンド技術まで、調べた内容を紹介します。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="クロスプラットフォーム" scheme="https://future-architect.github.io/tags/%E3%82%AF%E3%83%AD%E3%82%B9%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0/"/>
  </entry>
  <entry>
    <title>新しいマルチプラットフォームフレームワークLynxを触ってみた。</title>
    <link href="https://future-architect.github.io/articles/20250902a/"/>
    <id>https://future-architect.github.io/articles/20250902a/</id>
    <published>2025-09-01T15:00:00.000Z</published>
    <updated>2025-09-01T15:00:00.000Z</updated>
    <author><name>永井優斗</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250902a/image.png" alt="image.png" width="1200" height="347">

<p>夏の自由研究2025ブログ連載の6日目です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは、HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>の永井優斗です。</p>
<p>2025年3月にTikTokの運営会社である、ByteDance社が Lynxというクロスプラットフォームフレームワークを公開しました。</p>
<p>LynxはJavaScriptベースでのマルチプラットフォームフレームワークです。そう書かれると、React Nativeを思い浮かべない人はいないでしょう。React Nativeの競合になりうるフレームワークです。</p>
<p>まだまだLynx自体の知名度は低く、LynxとGoogle検索すると、元の意味であるオオヤマネコが出てきます。リンクスティップ（耳の先から出ている、狩りに役立つ毛）が可愛いですね。</p>
<img src="/images/2025/20250902a/image_2.png" alt="image.png" width="700" height="473" loading="lazy">

<ul>
<li>Lynx lynx, Nationalpark Bayerischer Wald, Deutschland の写真</li>
<li>著作者：Martin Mecnarowski</li>
<li>CC表示-継承 3.0 https://commons.wikimedia.org/wiki/File:Lynx_lynx_1_(Martin_Mecnarowski).jpg による</li>
</ul>
<p>この記事では、Lynx を触ってみた体験を簡単にまとめます。なお、開発した環境は、私物のMac mini(2020 M1)で、iPhone上での動作を試してみました。</p>
<h2 id="セットアップ">セットアップ</h2><p>※セットアップにはNode.js 18以上がインストールされている必要があります。</p>
<p>まずは公式の <code>create-rspeedy</code> でプロジェクトを作成します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm create rspeedy@latest</span><br><span class="line"><span class="built_in">cd</span> &lt;project-name&gt;</span><br><span class="line">npm install</span><br><span class="line">npm run dev</span><br></pre></td></tr></table></figure>

<ul>
<li>セットアップについては、公式ドキュメントのQuick Startもご確認ください</li>
</ul>
<p><code>npm run dev</code>でビルド成功後、QRコードとURLがターミナルに表示されます。</p>
<p>動作確認には、「Lynx Explorer」というアプリが必要です。</p>
<p>公式サイトに記載の「Download LynxExplorer-arm64.app.tar.gz.」のリンクから落として解凍し、XCodeのシミュレーターにドロップすることで、アプリをシミュレーターにインストールできます。ターミナルに表示されたURLをシミュレーター上の「Lynx Explorer」のCard URLに入力することで、動作確認ができます。</p>
<img src="/images/2025/20250902a/image_3.png" alt="image.png" width="450" height="899" loading="lazy">

<p>また、「Lynx Explorer」は、App Storeでも公開されており、スマホ実機にもインストールできます。</p>
<p>同じく、ターミナルに表示されたURLを入力するか、QR コードをアプリで読み込むと、そのままスマホ実機で動作確認できます。<br>開発している横で、ケーブルに繋ぐこともなく、即実機で試せるのは便利だなと感じました。</p>
<h2 id="カウンターアプリを作ってみた。">カウンターアプリを作ってみた。</h2><p>あまり時間をかけずに触ってみたいと思ったため、簡単なカウンターアプリを作成することにしました。</p>
<p>Lynx では HTML タグではなく、 <strong><code>&lt;view&gt;</code></strong> や <strong><code>&lt;text&gt;</code></strong> を使います。<br>イベントハンドラも React っぽくはなく、 <strong><code>bindtap</code></strong> , <strong><code>bindlongpress</code></strong> のように書くのが特徴的です。</p>
<figure class="highlight jsx"><figcaption><span>App.jsx</span></figcaption><table><tr><td class="code"><pre><span class="line">&lt;view <span class="keyword">class</span>=<span class="string">&quot;btn&quot;</span> bindtap=&#123;<span class="function">() =&gt;</span> <span class="title function_">setCount</span>(<span class="function"><span class="params">c</span> =&gt;</span> c + <span class="number">1</span>)&#125;&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">text</span>&gt;</span>+1<span class="tag">&lt;/<span class="name">text</span>&gt;</span></span></span><br><span class="line">&lt;/view&gt;</span><br></pre></td></tr></table></figure>

<p>CSS もそのまま使えるので、スタイリングは Web の知識でいけそうです。</p>
<h2 id="イベント拡張：「長押し」「ダブルタップ」">イベント拡張：「長押し」「ダブルタップ」</h2><p>次に、イベントAPIを活かしてカウンターを拡張しました。</p>
<ul>
<li><strong>+ボタン</strong> → タップで +1、長押しで +10</li>
<li><strong>–ボタン</strong> → タップで –1、ダブルタップで –10</li>
<li><strong>Resetボタン</strong> → 0 にリセット</li>
</ul>
<h3 id="ダブルタップ判定の実装例">ダブルタップ判定の実装例</h3><figure class="highlight jsx"><figcaption><span>App.jsx</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> lastTapTimeStampRef = <span class="title function_">useRef</span>(<span class="number">0</span>);</span><br><span class="line"><span class="keyword">const</span> singleTapTimeoutIdRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">DOUBLE_TAP_GAP</span> = <span class="number">250</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// -1 ボタンのtapハンドラ</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleMinusTap</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> now = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line">  <span class="keyword">const</span> gap = now - lastTapTimeStampRef.<span class="property">current</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span>(gap &lt; <span class="variable constant_">DOUBLE_TAP_GAP</span>)&#123;</span><br><span class="line">    <span class="comment">// ダブルタップと判定する</span></span><br><span class="line">    <span class="keyword">if</span>(singleTapTimeoutIdRef.<span class="property">current</span>) &#123;</span><br><span class="line">      <span class="comment">// ダブルタップが成立したらシングルタップ予約をキャンセル</span></span><br><span class="line">      <span class="built_in">clearTimeout</span>(singleTapTimeoutIdRef.<span class="property">current</span>);</span><br><span class="line">      singleTapTimeoutIdRef.<span class="property">current</span> = <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    lastTapTimeStampRef.<span class="property">current</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="title function_">setCount</span>(<span class="function">(<span class="params">c</span>) =&gt;</span> c - <span class="number">10</span>);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// シングルタップかもしれない場合は、少し待って確定する。</span></span><br><span class="line">  lastTapTimeStampRef.<span class="property">current</span> = now;</span><br><span class="line">  singleTapTimeoutIdRef.<span class="property">current</span> = <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">setCount</span>(<span class="function">(<span class="params">c</span>) =&gt;</span> c - <span class="number">1</span>);</span><br><span class="line">    lastTapTimeStampRef.<span class="property">current</span> = <span class="number">0</span>;</span><br><span class="line">    singleTapTimeoutIdRef.<span class="property">current</span> = <span class="literal">null</span>;</span><br><span class="line">  &#125;, <span class="variable constant_">DOUBLE_TAP_GAP</span>);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>標準で「ダブルタップ」イベントがあるわけではないので、<code>useRef</code> を使って「直前のタップ時刻」を保存し、一定時間内にもう一度押されたらダブルタップとみなすようにしました。</p>
<p>作成したアプリのUIはこんな感じになりました。</p>
<img src="/images/2025/20250902a/image_4.png" alt="image.png" width="520" height="1018" loading="lazy">

<p>GitHubでソースを公開しています。</p>
<h2 id="触ってみて感じた疎結合感">触ってみて感じた疎結合感</h2><p>カウンターアプリを作っていてまず感じたのは、<strong>「ReactとLynxががっちり結合しているわけではない」</strong> という点です。</p>
<p>イベントの書き方ひとつ取っても、React Native なら <code>onPress</code> を書くところを Lynx では <code>bindtap</code> を書きます。Reactのクリックイベントが<code>onClick</code>であるのに対してのReact Nativeの<code>onPress</code>はとてもわかりやすいなと思いますが、そのまま持ってきたという感じもします。</p>
<p>LynxはReactのAPIをそのまま持ってきたのではなく、Lynx側に独自のインターフェースがあり、それをReactでラップしているんだなと感じました。</p>
<p>また、スタイルが素のCSSで書けるのも印象的です。React Native のように StyleSheet API に閉じていないため、「Webの知識をそのまま活かせる」一方で「別のフロントエンドフレームワークからも同じ描画基盤を使える」余地があると感じました。</p>
<h2 id="React-Nativeとの違いとしてのフレームワーク依存性">React Nativeとの違いとしてのフレームワーク依存性</h2><p>React NativeはReactに強く結合しており、他のUIフレームワークを載せることはできません。Lynxは描画基盤とフレームワークが、（React Nativeと比較してという意味で）疎結合です。React 以外に他のJSフレームワークを載せることもできるかもしれません。</p>
<p>実際、公式のLynx公開時の説明でもLynxが対応するフレームワークReactに限らないことを表明しています。</p>
<blockquote>
<p>We are open-sourcing ReactLynx (“React on Lynx”) as Lynx’s initial frontend framework flavor, enabling componentized, declarative UI on Lynx.<br><strong>However, Lynx isn’t limited to React.</strong> In fact, other frameworks already represent roughly half of Lynx’s overall usage, demonstrating its neutrality in hosting different flavors.</p>
</blockquote>
<p>※太字はこちらでつけたものです。</p>
<p>実際、GitHub Issue #193 のとおりVue.jsをLynxに載せられないかというディスカッションはすでに始まっており、今後Vue.jsに対応するかもしれません。その話はまた、別の機会で深掘りしたいと思います。</p>
<h2 id="さいごに">さいごに</h2><p>Lynxはまだ3月に出たばかりでもあり、エコシステムの発展やLynx自体も「枯れる」までにはもうちょっと時間がかかるとは思います。<br>いますぐ業務で使えるというわけではないですが、今後React NativeやFlutterの対抗馬として、マルチプラットフォーム開発に使える日が来るかもしれません。</p>
<p>その頃にはVue.jsにも対応しているといいな。</p>
<h2 id="おまけ">おまけ</h2><p>今回Lynxを触るということまでは決めていたのですが、何つくろっかなーと悩んでいました。ChatGPT5と相談することで、簡単に試すことができました。</p>
<p>カウンターアプリを作ることや、作ったカウンターの拡張（長押し、ダブルタップ）はChatGPTのアイデアです。また、ChatGPTが生成したサンプルのソースコードに対して「この行何やってるんだろうか」と質問したり、逆にChatGPTが明らかにおかしいところを指摘しながら、対話を進めるとだいぶ理解が捗りました。</p>
<p>新しいプログラミング言語に入門するとき、LLMは大きなアシスタントになってくれるんだなと感じました。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">ByteDance社が LynxというLynxはJavaScriptベースのクロスプラットフォームフレームワークを公開しました。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="React" scheme="https://future-architect.github.io/tags/React/"/>
    <category term="クロスプラットフォーム" scheme="https://future-architect.github.io/tags/%E3%82%AF%E3%83%AD%E3%82%B9%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0/"/>
  </entry>
  <entry>
    <title>Xcode Cloudで最初に作りたい基本的なワークフロー</title>
    <link href="https://future-architect.github.io/articles/20250609a/"/>
    <id>https://future-architect.github.io/articles/20250609a/</id>
    <published>2025-06-08T15:00:00.000Z</published>
    <updated>2025-06-08T15:00:00.000Z</updated>
    <author><name>橋本竜我</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250609a/image.png" alt="" width="868" height="518">

<h2 id="はじめに">はじめに</h2><p>CI&#x2F;CD連載 4本目です。</p>
<p>HIGの橋本です。</p>
<p>Apple 純正のCI&#x2F;CDサービスである Xcode Cloud を初めて導入するときに、導入時にまず押さえておきたいポイントを実際に試し、まとめた記事です。</p>
<p>本記事では、次の3つについて述べます。</p>
<ol>
<li>リモートリポジトリへのプッシュをトリガーにCIを実行し、ビルドとテストを行う<code>Pull Request Workflow</code></li>
<li>Git タグのPushをトリガーとし、ビルド、テスト、TestFlight へアプリを配信する<code>TestFlight Release Workflow</code></li>
<li>成功・失敗を Slack へ通知する手順</li>
</ol>
<h2 id="Xcode-Cloud-概要">Xcode Cloud 概要</h2><p>Xcode Cloud は、Xcode に組み込まれた継続的インテグレーションおよびデリバリーサービスで、Apple デベロッパーのために設計された CI&#x2F;CD サービスです。署名まわりの煩雑な設定が不要になる点が最大の特徴です。これにより開発者はワークフロー最適化といった本質的な作業に集中でき、結果として開発体験が向上します。</p>
<p>Xcode Cloud - Apple Developer</p>
<p>主に次のような機能、特徴を持っています。</p>
<ul>
<li>自動ワークフロー<ul>
<li>開発プロセスに合わせてワークフローをカスタマイズすることが可能できます<br>トリガー、アクション（ビルド、テストなど）、通知などを設定できます。</li>
</ul>
</li>
<li>並列テスト<ul>
<li>複数プラットフォーム・複数デバイスでのテストを並列実行できます</li>
</ul>
</li>
<li>Xcode への組み込み<ul>
<li>Xcode から実行状況やクラッシュレポートをまとめて確認できます</li>
</ul>
</li>
<li>App Store Connect への組み込み<ul>
<li>App Store Connect でも同じ情報を閲覧でき、Workflow の編集も行えます</li>
</ul>
</li>
<li>Apple Developer Program メンバーシップ加入者は、毎月25時間分のコンピューティング時間を無料で利用できます</li>
</ul>
<div class="note-container note-warn"><span class="note-icon"></span><div>

<p><strong>注意</strong></p>
<p>Xcode Cloud の毎月25時間分無料枠は、2025年6月時点の情報になります。今後変更される可能性があります。最新情報は Apple 公式ページをご確認ください。</p>
</div></div>

<h2 id="Pull-Request-Workflow">Pull Request Workflow</h2><p>リモートリポジトリへのプッシュをトリガーにCIを実行し、ビルドとテストを行う<code>Pull Request Workflow</code>を作成します。</p>
<ul>
<li>Getting started with Xcode Cloud: Pull Request (PR) Workflow</li>
</ul>
<h3 id="ワークフローの設定">ワークフローの設定</h3><p>ワークフローで設定できる各種項目は次のとおりです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">大分類</th>
<th align="left">設定できる主な項目</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>General</strong></td>
<td align="left">ワークフローの名前、説明、編集権限、プライマリリポジトリ</td>
</tr>
<tr>
<td align="left"><strong>Environment</strong></td>
<td align="left">Xcode バージョン、 macOS バージョン、クリーンビルド、環境変数</td>
</tr>
<tr>
<td align="left"><strong>Start Conditions</strong></td>
<td align="left">ブランチの変更、PRの変更、タグの変更、手動開始</td>
</tr>
<tr>
<td align="left"><strong>Actions</strong></td>
<td align="left">分析、アーカイブ、ビルド、テスト</td>
</tr>
<tr>
<td align="left"><strong>Post-Actions</strong></td>
<td align="left">TestFlight 外部テスト、TestFlight 内部テスト、公証（macOS のみ）、通知</td>
</tr>
</tbody></table></div>
<p>以下では、Xcode での設定例を示します。</p>
<h4 id="General">General</h4><p>ワークフローの名前や説明を記載し、リポジトリ、プロジェクトを設定します。</p>
<img src="/images/2025/20250609a/スクリーンショット_2025-06-09_12.26.21.png" alt="スクリーンショット_2025-06-09_12.26.21.png" width="1200" height="793" loading="lazy">

<h4 id="Environment">Environment</h4><p>Xcode Version, macOS Version を最新に設定します（自身のプロジェクトに合わせて対象バージョンを修正してください）。</p>
<img src="/images/2025/20250609a/スクリーンショット_2025-06-09_12.26.35.png" alt="スクリーンショット_2025-06-09_12.26.35.png" width="1200" height="790" loading="lazy">

<h4 id="Start-Conditions-Pull-Request-Changes">Start Conditions (Pull Request Changes)</h4><p>feature ブランチで変更をコミットし、リモートブランチに Push したことをトリガーに CI が動作するように設定します。</p>
<ul>
<li>Sourceブランチを<code>feature/</code>で始まるブランチに設定</li>
<li>Targetブランチを<code>develop</code>ブランチに設定</li>
</ul>
<img src="/images/2025/20250609a/スクリーンショット_2025-06-09_12.26.47.png" alt="スクリーンショット_2025-06-09_12.26.47.png" width="1200" height="791" loading="lazy">

<h4 id="Actions">Actions</h4><p>トリガーを検知したときに、実行する内容をここで決めます。本ワークフローではビルドとテストを追加します。</p>
<ol>
<li>Build - iOS  <img src="/images/2025/20250609a/スクリーンショット_2025-06-09_12.27.05.png" alt="スクリーンショット_2025-06-09_12.27.05.png" width="1200" height="793" loading="lazy"></li>
<li>Test - iOS  <img src="/images/2025/20250609a/スクリーンショット_2025-06-09_12.27.14.png" alt="スクリーンショット_2025-06-09_12.27.14.png" width="1200" height="793" loading="lazy"></li>
</ol>
<h4 id="Post-Actions">Post-Actions</h4><p>Actions の実行完了後の処理を定義します。ここでは、指定の Slack チャンネルへ成功・失敗の通知が届くように設定します。</p>
<img src="/images/2025/20250609a/スクリーンショット_2025-06-09_12.27.48.png" alt="スクリーンショット_2025-06-09_12.27.48.png" width="1200" height="790" loading="lazy">

<h3 id="動作イメージ">動作イメージ</h3><p>feature ブランチで改修し、develop ブランチへのPRを作成したときの CI 動作イメージを示します。</p>
<p>まず、 feature ブランチで変更をコミットし、リモートブランチに Push します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">git checkout -b feature/7-modify-title</span><br><span class="line">git add .</span><br><span class="line">git commit -m <span class="string">&quot;fix: modify navigation title&quot;</span></span><br><span class="line">git push origin feature/7-modify-title</span><br></pre></td></tr></table></figure>

<p>GitHub 上で PR を作成します。</p>
<p>トリガーである<code>feature/</code>で始まるブランチから<code>develop</code>へのPRが作成されたことを検知し、XcodeとGitHub上でCIが動作していることが確認できます。</p>
<p>Xcode</p>
<img src="/images/2025/20250609a/スクリーンショット_2025-06-09_12.37.03.png" alt="スクリーンショット_2025-06-09_12.37.03.png" width="1200" height="725" loading="lazy">

<p>GitHub</p>
<img src="/images/2025/20250609a/スクリーンショット_2025-06-09_12.36.39.png" alt="スクリーンショット_2025-06-09_12.36.39.png" width="1200" height="378" loading="lazy">

<p>Build と Test が成功すると、Xcode で次のように結果概要が確認できます。</p>
<img src="/images/2025/20250609a/スクリーンショット_2025-06-09_12.48.51.png" alt="スクリーンショット_2025-06-09_12.48.51.png" width="1200" height="725" loading="lazy">

<p>GitHub の PR 上で次のように、Pull Request Workflow が成功していることも確認できます。</p>
<img src="/images/2025/20250609a/image_2.png" alt="image.png" width="1200" height="478" loading="lazy">

<p>また、成功通知が Slack に届いていることも確認できます。</p>
<img src="/images/2025/20250609a/スクリーンショット_2025-06-09_12.50.53.png" alt="スクリーンショット_2025-06-09_12.50.53.png" width="1200" height="359" loading="lazy">

<h2 id="TestFlight-Release-Workflow">TestFlight Release Workflow</h2><p>シンプルなGitタグのPushをトリガーとする、ビルド、テスト、アーカイブ、TestFlightリリースする <code>TestFlight Release Workflow</code>を作成します。</p>
<h3 id="ワークフローの設定-1">ワークフローの設定</h3><p>ワークフローを次のように設定します。ここでは、Xcode 上から設定する例を示します。<br>重要な点としては、TestFlight リリースするために、Actions に Archive を必ず追加します。</p>
<h4 id="General-1">General</h4><p>ワークフローの名前を<code>TestFlight Release Workflow</code>とします。</p>
<img src="/images/2025/20250609a/image_3.png" alt="image.png" width="1200" height="792" loading="lazy">

<h4 id="Environment-1">Environment</h4><p>Xcode Version, macOS Version を最新に設定します（自身のプロジェクトに合わせて対象バージョンを修正してください）。</p>
<img src="/images/2025/20250609a/image_4.png" alt="image.png" width="1200" height="792" loading="lazy">

<h4 id="Start-Conditions-Tag-Changes">Start Conditions (Tag Changes)</h4><p>Git タグの Push をトリガーとするため、Tag Changes を選択します。<br>ここでは、TagはAny Tag としています（Tag の命名ルールがある場合は、Custom Tags から指定できます）。</p>
<img src="/images/2025/20250609a/image_5.png" alt="image.png" width="1200" height="788" loading="lazy">

<h4 id="Actions-1">Actions</h4><p>TestFlight リリースする本ワークフローではビルドとテストに加えて、アーカイブも追加します。</p>
<ol>
<li>Build - iOS  <img src="/images/2025/20250609a/image_6.png" alt="image.png" width="1200" height="789" loading="lazy"></li>
<li>Test - iOS  <img src="/images/2025/20250609a/image_7.png" alt="image.png" width="1200" height="792" loading="lazy"></li>
<li>Archive - iOS  <img src="/images/2025/20250609a/image_8.png" alt="image.png" width="1200" height="787" loading="lazy"></li>
</ol>
<h4 id="Post-Actions-1">Post-Actions</h4><p>Slack への通知するNotify、内部テスター向けTest Flight リリースする TestFlight Internal Testing を追加します。</p>
<ol>
<li>Notify - Successes and Failures  <img src="/images/2025/20250609a/image_9.png" alt="image.png" width="1200" height="789" loading="lazy"></li>
<li>TestFlight Internal Testing - iOS<br>  内部テスターグループを選択します。（事前に、App Store Connect上で内部テスターの登録が必要です。）  <img src="/images/2025/20250609a/image_10.png" alt="image.png" width="1200" height="789" loading="lazy"></li>
</ol>
<h4 id="動作イメージ-1">動作イメージ</h4><p><code>release</code>ブランチにチェックアウトし、タグを作成して<code>Push</code>します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">git checkout release</span><br><span class="line">git tag v1.0.0</span><br><span class="line">git push origin v1.0.0</span><br></pre></td></tr></table></figure>

<p>トリガーを検知し、CI が動作します。</p>
<p>Xcode</p>
<img src="/images/2025/20250609a/image_11.png" alt="image.png" width="1200" height="725" loading="lazy">

<p>App Store Connect</p>
<img src="/images/2025/20250609a/image_12.png" alt="image.png" width="1200" height="751" loading="lazy">

<p>ワークフローが無事成功しました。Xcode, App Store Connect ではそれぞれ次のように成功していることを確認できます。</p>
<p>Xcode</p>
<img src="/images/2025/20250609a/image_13.png" alt="image.png" width="1200" height="725" loading="lazy">

<p>App Store Connect</p>
<img src="/images/2025/20250609a/image_14.png" alt="image.png" width="1200" height="783" loading="lazy">

<p>App Store Connect の TestFlight タブに新しいビルドが作成され、ステータス「テスト中」となり、内部テスターに配信されていることが確認できます。</p>
<img src="/images/2025/20250609a/image_15.png" alt="image.png" width="1200" height="459" loading="lazy">

<p>また、Slack へ本ワークフローが成功したことを伝える通知が届きます。</p>
<img src="/images/2025/20250609a/image_16.png" alt="image.png" width="1104" height="306" loading="lazy">

<p>これで TestFlight リリースしたいブランチにタグを Push するだけで、簡単に TestFlight リリースができるようになりました。</p>
<h5 id="Tips">Tips</h5><p>検証中、Archive は正常に完了しても TestFlight への配信が一向に進まない事象に遭遇しましたが、以下の記事で紹介されている手順で解消できました。</p>
<ul>
<li>Xcode CloudでArchiveは成功するのにTestFlightへの配信が進まないとき</li>
</ul>
<p>原因：</p>
<blockquote>
<p>アプリの新しいバージョンを提出するたびに、App Store Connectはコンプライアンスレビューを案内する質問をします。アプリInformation Property Listファイルに必要な情報を提供することで、これらの質問を回避し、提出プロセスを合理化できます。</p>
</blockquote>
<p>解決策：</p>
<blockquote>
<p>アプリが暗号化を使用していない場合は、<code>Info.plist</code>ファイルに<code>ITSAppUsesNonExemptEncryption</code>キーを追加し、値を<code>NO</code>に設定します。</p>
</blockquote>
<h3 id="成功・失敗をSlackへ通知する設定手順">成功・失敗をSlackへ通知する設定手順</h3><p>次の Apple 公式ドキュメントを参考に、成功・失敗を Slack へ通知するように設定してみます。</p>
<ul>
<li>Connecting Xcode Cloud to Slack | Apple Developer Documentation</li>
</ul>
<p>まず、Slack への通知したい WorkFlow の編集画面を開き、Post-Actions に Notify を追加します。</p>
<img src="/images/2025/20250609a/image_17.png" alt="image.png" width="965" height="634" loading="lazy">

<p>画面キャプチャ中の Slack の下の「＋」ボタンを押します。</p>
<img src="/images/2025/20250609a/image_18.png" alt="image.png" width="960" height="630" loading="lazy">

<p>Slackのワークスペースと接続するか問われるダイアログが表示されるので、「Connect…」を押します。</p>
<img src="/images/2025/20250609a/image_19.png" alt="image.png" width="1200" height="623" loading="lazy">

<p>ブラウザが立ち上がり、Xcode Cloud に Slack の任意のワークスペースにアクセスする権限を付与してよいか確認されます。<br>問題なければ、「許可」をクリックします。</p>
<img src="/images/2025/20250609a/image_20.png" alt="image.png" width="962" height="633" loading="lazy">

<p>Xcode に戻り、アクセス権限を付与したワークスペースのチャンネルが表示されます。<br>通知を送信したいチャンネルを選択し、「OK」を押します。</p>
<img src="/images/2025/20250609a/image_21.png" alt="image.png" width="1200" height="789" loading="lazy">

<p>「Save」をクリックして変更を保存します。<br>これで CI&#x2F;CD の実行結果が Slack へ通知されるようになります。</p>
<p><strong>成功したときの通知例</strong></p>
<img src="/images/2025/20250609a/image_22.png" alt="image.png" width="592" height="173" loading="lazy">

<p>とても簡単に設定ができました。<br>この通知から、App Store Connect の Xcode Cloud のページを直接開いたり、該当の PR を開いたり出来るのもとても便利です。</p>
<h2 id="さいごに">さいごに</h2><p>Xcode Cloud でまず作りたい基本的な CI ワークフローとして、2種類のワークフローを作成してみました。</p>
<p>今回作成した Pull Request Workflow と TestFlight Release Workflow や、当社の有志メンバーで作成したGitブランチフロー規約などを参考に、皆さんのプロジェクトで最適な CI&#x2F;CD ワークフローを考えてみるのはいかがでしょうか。</p>
<p>Xcode Cloud でできることはまだたくさんあるので、知見がたまり次第また記事にしたいと思います。</p>
<h2 id="参考">参考</h2><ul>
<li>Xcode Cloud - Apple Developer</li>
<li>Configuring your first Xcode Cloud workflow | Apple Developer Documentation</li>
<li>Getting started with Xcode Cloud: Pull Request (PR) Workflow</li>
<li>Connecting Xcode Cloud to Slack | Apple Developer Documentation</li>
<li>Xcode CloudでArchiveは成功するのにTestFlightへの配信が進まないとき</li>
</ul>
]]></content>
    <summary type="html">Apple 純正のCI/CDサービスである Xcode Cloud を初めて導入するときに、導入時にまず押さえておきたいポイントを実際に試し、まとめた記事です。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="CI/CD" scheme="https://future-architect.github.io/tags/CI-CD/"/>
    <category term="Xcode" scheme="https://future-architect.github.io/tags/Xcode/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
  </entry>
  <entry>
    <title>iOS ユニバーサルリンクをAzure BlobStorageでやってみる</title>
    <link href="https://future-architect.github.io/articles/20250527b/"/>
    <id>https://future-architect.github.io/articles/20250527b/</id>
    <published>2025-05-26T15:00:01.000Z</published>
    <updated>2025-05-26T15:00:01.000Z</updated>
    <author><name>高山博司</name></author>
    <content type="html"><![CDATA[<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>この記事はグループ会社であるFutureOneの Qiita Organizationで公開された 記事 をクロスポストで公開しています。</p>
</div></div>

<h2 id="はじめに">はじめに</h2><p>.NET MAUIでiOS向けアプリを開発中、WebアプリからiOSアプリを呼び出す必要がでてきたため、ユニバーサルリンクを実装しました。WebサーバーはAzure Blob Storageの静的サイトを利用します。</p>
<h2 id="ユニバーサルリンクとは">ユニバーサルリンクとは</h2><p>ユニバーサルリンク（Universal Links）は、Webサイトへのリンクをクリックしたときに、そのリンクに対応するiOSアプリがインストールされていれば直接アプリを起動し、インストールされていなければWebサイトを表示するという機能です。</p>
<ul>
<li>参考: Allowing apps and websites to link to your content</li>
</ul>
<h3 id="仕組み">仕組み</h3><ol>
<li>WebサーバーにApple App Site Association （以下: AASA）ファイルを置いておきます</li>
<li>Apple CDNが定期的にWebサーバーのAASAファイルをクロールします</li>
<li>iPhoneが、アプリインストール時にAppleCDNからAASAファイルを取得します</li>
<li>Webサイト上で特定のURLを踏むとAASAファイルを使ってアプリを立ち上げる</li>
</ol>
<p>簡単に図示すると以下のような感じになります。</p>
<img fetchpriority="high" src="/images/2025/20250527b/mermaid-diagram-2025-05-27-102746.png" width="800" height="644">

<h2 id="WebサーバーにAzure-Blob-Storageを利用する">WebサーバーにAzure Blob Storageを利用する</h2><p>上記の通り、AppleCDNがアクセスできるWebサーバを準備し、UniversalLink用のAASAファイルを配備する必要があります。</p>
<p>そこで、安価に静的サイトを公開できるAzure BlobStorageの静的サイト機能を使いたいと思います。</p>
<ul>
<li>参考: Azure Storage での静的 Web サイト ホスティング</li>
</ul>
<h2 id="Azure-Storage構築">Azure Storage構築</h2><h3 id="ストレージアカウントの作成">ストレージアカウントの作成</h3><p>まずはストレージアカウントを作成します。</p>
<p>ストレージアカウントは基本的にデフォルトで大丈夫だと思いますが、必要に応じて値を変更します。基本情報のプライマリサービスは 「Azure Blob Storage または Azure Data Lake Storage Gen 2」 とします。</p>
<img src="/images/2025/20250527b/image.png" alt="image.png" width="1200" height="647" loading="lazy">

<p>作ったものがこちらです。アカウントの種類が <code>StorageV2 (汎用 v2)</code>  であることを確認します（上位のものでも大丈夫です）。</p>
<img src="/images/2025/20250527b/image_2.png" alt="image.png" width="1200" height="279" loading="lazy">

<h3 id="静的サイトを有効化">静的サイトを有効化</h3><p>概要の下の方の機能タブから静的Webサイトを選択します。</p>
<img src="/images/2025/20250527b/image_3.png" alt="image.png" width="1200" height="456" loading="lazy">

<p>ここで有効化します。プライマリエンドポイントが今回のUniversalLinkのドメインになります。<code>myapp-universal-link-sample.z11.web.core.windows.net</code> のような感じになると思います。</p>
<img src="/images/2025/20250527b/image_4.png" alt="image.png" width="1200" height="420" loading="lazy">

<p>※ カスタムドメインの使用もできますが、今回はスキップします。</p>
<p>静的サイトを有効化すると、<code>$Web</code> フォルダができます。</p>
<img src="/images/2025/20250527b/image_5.png" alt="image.png" width="1200" height="454" loading="lazy">

<h3 id="AASAファイルを配置">AASAファイルを配置</h3><p>AASAファイルを準備します。</p>
<ul>
<li>参考: Supporting associated domains</li>
</ul>
<p>内容は JSON ですが、ファイルに拡張子はつけないで保存します。ファイル名は <code>apple-app-site-association</code>  です。</p>
<p>今回はリンクURLによる制限はつけずにどんなURLでも呼び出し可能とします。</p>
<figure class="highlight json"><figcaption><span>apple-app-site-association</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;webcredentials&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;apps&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;アプリのID&quot;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;applinks&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;apps&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;details&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;appIDs&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">          <span class="string">&quot;アプリのID&quot;</span></span><br><span class="line">        <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;components&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">          <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;/&quot;</span><span class="punctuation">:</span> <span class="string">&quot;*&quot;</span></span><br><span class="line">          <span class="punctuation">&#125;</span></span><br><span class="line">        <span class="punctuation">]</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p><code>$Web</code> コンテナを開いてアップロードします。</p>
<p>右下のアップロード先のフォルダは <code>.well-known</code> にします。フォルダが存在しない場合でも、自動的に作成してくれます。</p>
<img src="/images/2025/20250527b/image_6.png" alt="image.png" width="1200" height="509" loading="lazy">

<p>アップロードしたファイルを確認します。</p>
<p>階層が <code>$Web/.well-known/apple-app-site-association</code> となっていればOKです。</p>
<img src="/images/2025/20250527b/image_7.png" alt="image.png" width="1200" height="251" loading="lazy">

<p>最後に <code>ContentType</code> を指定します。</p>
<p>ファイルをクリックするとプロパティが開くので編集します。<code>application/json; charset=utf-8</code> にします。<code>application/json</code> は必須だと思います。 charset&#x3D;utf-8はなくても大丈夫ですが、念の為に指定します。</p>
<img src="/images/2025/20250527b/image_8.png" alt="image.png" width="1200" height="606" loading="lazy">

<h3 id="WebをPublic化">$WebをPublic化</h3><p>Apple CDNがアップロードしたAASAファイルを取得できるようにします。 コンテナを選択してアクセスレベルを変更します。 デフォルトだとプライベートになっていると思うので、コンテナしておきます。</p>
<img src="/images/2025/20250527b/image_9.png" alt="image.png" width="1200" height="194" loading="lazy">

<p>AASAファイルを確認します。</p>
<p>先程のプライマリエンドポイントに <code>.well-known/apple-app-site-association</code> つけてURLバーに入れて（例: <code>https://myapp-universal-link-sample/.well-known/apple-app-site-association</code>）、AASAファイルが見えることを確認します。</p>
<img src="/images/2025/20250527b/image_10.png" alt="image.png" width="1200" height="415" loading="lazy">

<h3 id="Apple-CDNを確認">Apple CDNを確認</h3><p>アップロード後、しばらくするとAASAファイルをApple CDNが回収してくれます。</p>
<p>そのキャッシュは <code>https://app-site-association.cdn-apple.com/a/v1/myapp-universal-link-sample.z11.web.core.windows.net</code> で確認できます。</p>
<img src="/images/2025/20250527b/image_11.png" alt="image.png" width="731" height="569" loading="lazy">

<p>以上で環境構築は完了です。</p>
<h2 id="Mauiアプリの設定">Mauiアプリの設定</h2><p>MauiプロジェクトのPlatformフォルダのIOSフォルダ直下に <code>Entitlements.plist</code> を作成します。内容は以下の通りです。</p>
<p>先程のプライマリエンドポイントのを記載します。「https:&#x2F;&#x2F;」は不要です。</p>
<figure class="highlight xml"><figcaption><span>Entitlements.plist</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;UTF-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">plist</span> <span class="keyword">PUBLIC</span> <span class="string">&quot;-//Apple//DTD PLIST 1.0//EN&quot;</span> <span class="string">&quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">plist</span> <span class="attr">version</span>=<span class="string">&quot;1.0&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">dict</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">key</span>&gt;</span>com.apple.developer.associated-domains<span class="tag">&lt;/<span class="name">key</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">array</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">string</span>&gt;</span>applinks:myapp-universal-link-sample.z11.web.core.windows.net<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">array</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">dict</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">plist</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><code>Entitlements.plist</code> を作りたくない人はcsprjで設定もできます。</p>
<p>以下は、Apple universal linksより引用したコードです。</p>
<figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">ItemGroup</span> <span class="attr">Condition</span>=<span class="string">&quot;$([MSBuild]::GetTargetPlatformIdentifier(&#x27;$(TargetFramework)&#x27;)) == &#x27;ios&#x27; Or $([MSBuild]::GetTargetPlatformIdentifier(&#x27;$(TargetFramework)&#x27;)) == &#x27;maccatalyst&#x27;&quot;</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">&lt;!-- For debugging, use &#x27;?mode=developer&#x27; for debug to bypass apple&#x27;s CDN cache --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">CustomEntitlements</span></span></span><br><span class="line"><span class="tag">        <span class="attr">Condition</span>=<span class="string">&quot;$(Configuration) == &#x27;Debug&#x27;&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">Include</span>=<span class="string">&quot;com.apple.developer.associated-domains&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">Type</span>=<span class="string">&quot;StringArray&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">Value</span>=<span class="string">&quot;applinks:myapp-universal-link-sample.z11.web.core.windows.net?mode=developer&quot;</span> /&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">&lt;!-- Non-debugging, use normal applinks:url value --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">CustomEntitlements</span></span></span><br><span class="line"><span class="tag">        <span class="attr">Condition</span>=<span class="string">&quot;$(Configuration) != &#x27;Debug&#x27;&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">Include</span>=<span class="string">&quot;com.apple.developer.associated-domains&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">Type</span>=<span class="string">&quot;StringArray&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">Value</span>=<span class="string">&quot;applinks:myapp-universal-link-sample.z11.web.core.windows.net&quot;</span> /&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">ItemGroup</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>※ <code>?mode=developer</code> をつけておくとApple CDNをバイパスして、直接WebサーバからAASAファイルを取得するようになります。</p>
<p>次に、UniversalLinkで起動されたときのハンドラーを書きます。</p>
<p>Apple universal links - .NET MAUI | Microsoft Learn を参考に<code>MauiProgram.cs</code> を編集し、ユニバーサルリンク経由でアプリが起動・再開された際にURLを処理するためのライフサイクルイベントを登録します。</p>
<figure class="highlight cs"><figcaption><span>MauiProgram.cs</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.Maui.LifecycleEvents;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Logging;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyNamespace</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title">MauiProgram</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> MauiApp <span class="title">CreateMauiApp</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">var</span> builder = MauiApp.CreateBuilder();</span><br><span class="line">        builder</span><br><span class="line">            .UseMauiApp&lt;App&gt;()</span><br><span class="line">            .ConfigureFonts(fonts =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                fonts.AddFont(<span class="string">&quot;OpenSans-Regular.ttf&quot;</span>, <span class="string">&quot;OpenSansRegular&quot;</span>);</span><br><span class="line">                fonts.AddFont(<span class="string">&quot;OpenSans-Semibold.ttf&quot;</span>, <span class="string">&quot;OpenSansSemibold&quot;</span>);</span><br><span class="line">            &#125;)</span><br><span class="line">            .ConfigureLifecycleEvents(lifecycle =&gt;</span><br><span class="line">            &#123;</span><br><span class="line"><span class="meta">#<span class="keyword">if</span> IOS || MACCATALYST</span></span><br><span class="line">                lifecycle.AddiOS(ios =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="comment">// Universal link delivered to FinishedLaunching after app launch.</span></span><br><span class="line">                    ios.FinishedLaunching((app, data) =&gt; HandleAppLink(app.UserActivity));</span><br><span class="line"></span><br><span class="line">                    <span class="comment">// Universal link delivered to ContinueUserActivity when the app is running or suspended.</span></span><br><span class="line">                    ios.ContinueUserActivity((app, userActivity, handler) =&gt; HandleAppLink(userActivity));</span><br><span class="line"></span><br><span class="line">                    <span class="comment">// Only required if using Scenes for multi-window support.</span></span><br><span class="line">                    <span class="keyword">if</span> (OperatingSystem.IsIOSVersionAtLeast(<span class="number">13</span>) || OperatingSystem.IsMacCatalystVersionAtLeast(<span class="number">13</span>))</span><br><span class="line">                    &#123;</span><br><span class="line">                        <span class="comment">// Universal link delivered to SceneWillConnect after app launch</span></span><br><span class="line">                        ios.SceneWillConnect((scene, sceneSession, sceneConnectionOptions)</span><br><span class="line">                            =&gt; HandleAppLink(sceneConnectionOptions.UserActivities.ToArray()</span><br><span class="line">                                .FirstOrDefault(a =&gt; a.ActivityType == Foundation.NSUserActivityType.BrowsingWeb)));</span><br><span class="line"></span><br><span class="line">                        <span class="comment">// Universal link delivered to SceneContinueUserActivity when the app is running or suspended</span></span><br><span class="line">                        ios.SceneContinueUserActivity((scene, userActivity) =&gt; HandleAppLink(userActivity));</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;);</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">if</span> DEBUG</span></span><br><span class="line">        builder.Logging.AddDebug();</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> builder.Build();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">if</span> IOS || MACCATALYST</span></span><br><span class="line">    <span class="function"><span class="keyword">static</span> <span class="built_in">bool</span> <span class="title">HandleAppLink</span>(<span class="params">Foundation.NSUserActivity? userActivity</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (userActivity <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">null</span> &amp;&amp; userActivity.ActivityType == Foundation.NSUserActivityType.BrowsingWeb &amp;&amp; userActivity.WebPageUrl <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">null</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            HandleAppLink(userActivity.WebPageUrl.ToString());</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">HandleAppLink</span>(<span class="params"><span class="built_in">string</span> url</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, <span class="keyword">out</span> <span class="keyword">var</span> uri))</span><br><span class="line">            App.Current?.SendOnAppLinkRequestReceived(uri);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>Appクラスの <code>OnAppLinkRequestReceived</code> を呼んでくれるのでOverrideすることで受け取ったURLのハンドルができます。<br>クエリパラメータもらうときなどはここで処理するとよいと思います。</p>
<figure class="highlight cs"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">OnAppLinkRequestReceived</span>(<span class="params">Uri uri</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">   <span class="comment">//uriの処理が必要であれば</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>Mauiアプリの修正は以上です。</p>
<h2 id="Apple-DeveloperアカウントのアプリIDに、関連ドメインの使用を許可">Apple DeveloperアカウントのアプリIDに、関連ドメインの使用を許可</h2><p>UniversalLinkを使うにはApple上のアプリに使用を許可する必要があります。</p>
<p>手順は以下の通りです（参考: Apple universal links）。</p>
<ol>
<li>Web ブラウザでApple Developer アカウントにログインし、 「証明書、ID、およびプロファイル」ページに移動します</li>
<li>「証明書、識別子、プロファイル」ページで、「識別子」タブを選択します</li>
<li>「識別子」ページで、アプリに対応するアプリ ID を選択します</li>
<li>「App ID 構成の編集」ページで、「関連ドメイン」機能を有効にし、「保存」ボタンを選択します<img src="/images/2025/20250527b/image_12.png" alt="image.png" width="934" height="735" loading="lazy"></li>
<li>profileに反映させます。Editから先程設定したApp IDを選択します<br>Enabled CapacitiesにAssociated DomainがあればOKです<img src="/images/2025/20250527b/image_13.png" alt="image.png" width="941" height="473" loading="lazy"></li>
<li>最後にVisual Studioのプロファイルを更新します</li>
</ol>
<h2 id="アプリをリリースする">アプリをリリースする</h2><p>あとはアプリをリリースするだけです。デバッグでも大丈夫です。</p>
<p>今回の場合のユニバーサルリンクのURLはブロブストレージのプライマリエンドポイント（例: <code>https://myapp-universal-link-sample.z11.web.core.windows.net/</code>）になります。これをWebサイトに埋め込んでおきます。</p>
<p>アプリがインストールされている状態であれば、iPhoneのメモ帳でも確認ができます。リンク用のURLを打ち込んで長押しするとアプリを開くという選択肢がでてきます。</p>
<img src="/images/2025/20250527b/image_14.png" alt="image.png" width="600" height="1300" loading="lazy">

<h2 id="さいごに">さいごに</h2><p>Azure Blob Storageの静的サイト機能で簡単にUniversalLink用のWebサーバを立ち上げられました。</p>
<p>Azure環境がある場合は選択肢に入れてもいいかなと思います。</p>
]]></content>
    <summary type="html">.NET MAUIでiOS向けアプリを開発中、WebアプリからiOSアプリを呼び出す必要がでてきたため、ユニバーサルリンクを実装しました。WebサーバーはAzure Blob Storageの静的サイトを利用します。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term=".NET" scheme="https://future-architect.github.io/tags/NET/"/>
    <category term="Azure" scheme="https://future-architect.github.io/tags/Azure/"/>
    <category term="FutureOne" scheme="https://future-architect.github.io/tags/FutureOne/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
  </entry>
  <entry>
    <title>VS CodeによるiOSアプリ開発入門〜ビルド＆ラン〜</title>
    <link href="https://future-architect.github.io/articles/20250425a/"/>
    <id>https://future-architect.github.io/articles/20250425a/</id>
    <published>2025-04-24T15:00:00.000Z</published>
    <updated>2025-04-24T15:00:00.000Z</updated>
    <author><name>清水雄一郎</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは。HealthCare Innovation Group(HIG)の清水雄一郎です。<br>VSCodeだと思っていたのですが、厳密（？）にはVS Codeであることに気づいた今日この頃です。</p>
<details><summary>参考画像: MicrosoftのVS Codeダウンロードサイトでの表記</summary>

<p>https://azure.microsoft.com/ja-jp/products/visual-studio-code<br><img fetchpriority="high" src="/images/2025/20250425a/image.png" alt="image.png" width="842" height="518"></p>
</details>

<p>春の入門祭り2025 10日目の記事です。</p>
<p>SwiftにおけるiOS開発といえば、やはりXcodeを使っている方が多いと思います。</p>
<p>ただ最近は、VS Codeに魅力的な拡張機能（特に生成AIを活用したもの）が多く、活用していきたいと思うようになりました。</p>
<p>「Xcodeを最小限に抑えて、VS CodeでiOSアプリ開発してみたらどうなるんだろう？」という好奇心から、今回ちょっとした実験をしてみることにしました。</p>
<p>この記事では、VS Code（周辺ツール）だけでどこまでiOSアプリ開発するためにどのような準備が必要か、検証した記録をまとめていきます。</p>
<h2 id="環境">環境</h2><ul>
<li>MacBook Air (M2, 2022)</li>
<li>macOS 15.4.1</li>
<li>Xcode 16.3</li>
</ul>
<h2 id="事前準備">事前準備</h2><p>VS Code上で、次の拡張機能をインストールします。</p>
<ul>
<li>Swift - Visual Studio Marketplace</li>
</ul>
<p>また、検証用にXcode上で<code>TechBlogSpring2025</code>というiOSアプリプロジェクトを作成しておきます。</p>
<ul>
<li>Creating an Xcode project for an app | Apple Developer Documentation</li>
</ul>
<h2 id="ゴール">ゴール</h2><p>iOSアプリがビルド＆実行できるようになること。<br>※テストやコード補完などは、本記事では扱いません。</p>
<h3 id="ビルド">ビルド</h3><p>まずはビルドです。Xcode上では、<code>⌘ + B</code> に相当するものです。</p>
<p>VS Codeにはそういったショートカットキーはないため、CLIから実行していきます。</p>
<p>Xcodeがインストールされている場合は、<code>xcodebuild</code>コマンドが利用できると思いますので、そちらを利用して実行します。</p>
<p>今回サンプルで用意したプロジェクトを用いてiOSシミュレータ用にアプリをビルドする場合、次のようなコマンドになるかと思います。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">xcodebuild -workspace TechBlogSpring2025.xcodeproj/project.xcworkspace \</span><br><span class="line">           -scheme TechBlogSpring2025 \</span><br><span class="line">           -sdk iphonesimulator \</span><br><span class="line">           -configuration Debug \</span><br><span class="line">           build</span><br><span class="line"></span><br><span class="line">~~ 省略 ~~</span><br><span class="line"></span><br><span class="line">** BUILD SUCCEEDED **</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>無事ビルド成功しましたが、毎回このコマンドを打つのは手間です。解消するために、VS Codeには外部ツールを実行する方法としてタスクを登録できる<sup id="fnref:1">1</sup>ため、そちらを用意します。</p>
<figure class="highlight json"><figcaption><span>tasks.json</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;tasks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;shell&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Xcode Build - Debug&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;detail&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xcodebuild debug&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xcodebuild&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">                <span class="string">&quot;-workspace&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;TechBlogSpring2025.xcodeproj/project.xcworkspace&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;-scheme&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;TechBlogSpring2025&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;-sdk&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;iphonesimulator&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;-configuration&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;Debug&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;build&quot;</span></span><br><span class="line">            <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;build&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>タスクを登録しておくと、<code>⌘ + ⇧ + B</code> で簡単にビルドを実行できるようになります🎉</p>
<img src="/images/2025/20250425a/スクリーンショット_2025-04-25_7.03.56.png" alt="" width="1200" height="194" loading="lazy">

<h3 id="実行">実行</h3><p>次は実行です。これが結構手間でした。</p>
<p>Xcode上では、<code>⌘ + R</code> に相当するものです。実行するスキーマと端末（今回はシミュレータ）を選択して<code>⌘ + R</code>を押せばアプリがビルドされ、シミュレータ上でアプリが起動します。</p>
<img src="/images/2025/20250425a/スクリーンショット_2025-04-25_7.30.55.png" alt="" width="1200" height="750" loading="lazy">

<p>見慣れた光景ではあるのですが、Xcodeなしで同じことをやろうとするといくつかのステップが必要です。</p>
<blockquote>
<p>アプリがビルドされ、シミュレータ上でアプリが起動します。</p>
</blockquote>
<p>これを分解すると、次のようなステップになります。</p>
<ol>
<li>アプリをビルドする</li>
<li>実行したい機種のシミュレータを起動する</li>
<li>起動したシミュレータにアプリをインストールする</li>
<li>シミュレータでアプリを起動する</li>
</ol>
<p>それぞれ順にCLI上で実行する場合は、次のようになります。</p>
<figure class="highlight sh"><figcaption><span>アプリをビルドして、シミュレータ上で起動する</span></figcaption><table><tr><td class="code"><pre><span class="line">xcodebuild -workspace TechBlogSpring2025.xcodeproj/project.xcworkspace \</span><br><span class="line">           -scheme TechBlogSpring2025 \</span><br><span class="line">           -sdk iphonesimulator \</span><br><span class="line">           -configuration Debug \</span><br><span class="line">           -derivedDataPath .build/ \ <span class="comment"># シミュレーターにアプリをインストールする際に必要なので、出力先を任意の場所に指定する</span></span><br><span class="line">           build</span><br><span class="line">xcrun simctl boot <span class="string">&quot;iPhone 16 Pro&quot;</span> <span class="comment"># 実行したい機種のシミュレータを起動する</span></span><br><span class="line">xcrun simctl install booted build/Debug-iphonesimulator/TechBlogSpring2025.app <span class="comment"># 起動したシミュレータにアプリをインストールする</span></span><br><span class="line">xcrun simctl launch booted <span class="string">&quot;com.example.TechBlogSpring2025&quot;</span> <span class="comment"># シミュレータでアプリを起動する</span></span><br></pre></td></tr></table></figure>

<p>これで晴れてビルドしたアプリをシミュレータ上で確認できます！</p>
<p>ただし、ビルドの時と同様にこれらを毎回実行するのは手間なのでVS Codeのタスクに登録してしまいましょう。</p>
<p>今回は4ステップあったので、4つのタスクを実行する必要があるのか？と思いました。</p>
<p>しかし、VS Codeのタスクには<code>dependsOn</code>プロパティを利用することで、登録した複数のタスクを1つにまとめて実行できるみたいです👏</p>
<p>以下はtasks.jsonの例です。</p>
<details><summary>変更後のtasks.json</summary>

<figure class="highlight json"><figcaption><span>tasks.json</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;tasks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;shell&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Xcode Build - Debug&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;detail&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xcodebuild debug&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xcodebuild&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">                <span class="string">&quot;-workspace&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;TechBlogSpring2025.xcodeproj/project.xcworkspace&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;-scheme&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;TechBlogSpring2025&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;-sdk&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;iphonesimulator&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;-configuration&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;Debug&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;-derivedDataPath&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;.build/&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;build&quot;</span></span><br><span class="line">            <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;build&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;shell&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Boot Simulator&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xcrun&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">                <span class="string">&quot;simctl&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;boot&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;iPhone 16 Pro&quot;</span></span><br><span class="line">            <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;test&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;shell&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Run App on Simulator&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xcrun&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">                <span class="string">&quot;simctl&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;install&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;booted&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;.build/Build/Products/Debug-iphonesimulator/TechBlogSpring2025.app&quot;</span></span><br><span class="line">            <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;test&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;dependsOn&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Xcode Build - Debug&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;shell&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Launch App on Simulator&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xcrun&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">                <span class="string">&quot;simctl&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;launch&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;booted&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;yu1Ro5.TechBlogSpring2025&quot;</span></span><br><span class="line">            <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;test&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;dependsOn&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Run App on Simulator&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;shell&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Build and Run Sequence&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;dependsOn&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">                <span class="string">&quot;Xcode Build - Debug&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;Boot Simulator&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;Run App on Simulator&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="string">&quot;Launch App on Simulator&quot;</span></span><br><span class="line">            <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;dependsOrder&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sequence&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;test&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

</details>

<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>シミュレータを起動後に、再度起動するタスクを実行すると既に起動しているためエラーとなり、2回目以降はシミュレータの起動をスキップするタスクを別途構成する必要がありそうです。</p>
</div></div>

<h2 id="試してみて">試してみて</h2><p>今回チャレンジできなかった部分も含めて、XcodeとVS Code(w&#x2F; 拡張機能)で開発する場合、次のように使い分けようかと考えています。当初はXcode離れしたいモチベーションがありましたが、PreviewやInstrumentsなど、代替が見つけられなかった機能もあるため、適宜使い分けることができるといいなと思います。</p>
<ul>
<li>Xcodeを使う時<ul>
<li>Preview<sup id="fnref:2">2</sup> 機能を使って、UIを作り込む</li>
<li>アプリのアップロードなど、アプリ署名に関わる操作をする時</li>
<li>Instruments<sup id="fnref:3">3</sup> 機能を使って、アプリのパフォーマンスを測定する時</li>
<li>アプリの開発用証明書やプロビジョニングプロファイルの管理や設定時<br>-&gt; Xcodeのプロジェクトファイルは、人間が目で見てわかるものではないため</li>
</ul>
</li>
<li>VS Codeを使う時<ul>
<li>業務ロジックやテストなどのUI以外の実装時<br>-&gt; 生成AI活用した拡張機能使いやすいだけでも、使う価値あると思いました</li>
<li>MarkdownやJSON、YAMLなど、Swiftファイル以外を編集する時</li>
</ul>
</li>
</ul>
<h2 id="まとめ">まとめ</h2><p>Xcodeの利用を最小限に抑えて、VS CodeでiOSアプリ開発してみようという話でした。</p>
<p>本記事で挑戦した範囲では、Xcodeを使わずにVS Codeを開発業務に利用するというレベルまでは到達していません。しかし、アプリをビルド＆実行するまでの内部的な仕組みの理解度が高まるいいきっかけになりました。</p>
<p>一方、執筆にあたり試行錯誤する中で、SweetPad<sup id="fnref:4">4</sup>という今回準備した諸々をいい感じにセットアップしてくれるVS Code拡張機能に出会いました。この記事で詳しく紹介するまで辿り着けなかったですが、興味ある方はぜひ調べてみてください。</p>
<p>最後まで読んでいただき、ありがとうございました。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">https://code.visualstudio.com/docs/debugtest/tasks</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">https://developer.apple.com/documentation/xcode/previewing-your-apps-interface-in-xcode</span> ↩</li><li id="fn:3"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;">https://help.apple.com/instruments/developer/mac/current/</span> ↩</li><li id="fn:4"><span style="vertical-align: top; padding-right: 10px;">4.</span><span style="vertical-align: top;">https://sweetpad.hyzyla.dev/</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">SwiftにおけるiOS開発といえば、やはりXcodeを使っている方が多いと思います。ただ最近は、VS Codeに魅力的な拡張機能（特に生成AIを活用したもの）が多く、活用していきたいと思うようになりました。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="VSCode" scheme="https://future-architect.github.io/tags/VSCode/"/>
    <category term="Xcode" scheme="https://future-architect.github.io/tags/Xcode/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
  </entry>
  <entry>
    <title>try! Swift Tokyo 2025に参加してきました！</title>
    <link href="https://future-architect.github.io/articles/20250416b/"/>
    <id>https://future-architect.github.io/articles/20250416b/</id>
    <published>2025-04-15T15:00:01.000Z</published>
    <updated>2025-04-15T15:00:01.000Z</updated>
    <author><name>橋本竜我</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250416b/9addbdc8-d932-4dfc-ab35-fb5a7cf56fd0.jpeg" alt="" width="1080" height="1210">

<h2 id="はじめに">はじめに</h2><p>HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>の橋本です。</p>
<p>初めてtry! Swift Tokyo 2025に参加してきました！</p>
<p>参加報告として、興味深かったセッションや参加して感じたことなどを共有します。</p>
<h2 id="try-Swift-Tokyoとは">try! Swift Tokyoとは</h2><img src="/images/2025/20250416b/image.png" alt="" width="1200" height="326" loading="lazy">

<p>公式HP の記載の通り、Swiftに関する国際カンファレンスです。今年は、2025年4月9日~11日の3日間で開催されました。</p>
<blockquote>
<p><strong>イベントについて</strong><br>Swiftを使った開発のコツや最新の事例を求めて世界中から開発者が集います。<br>日頃のSwiftの知識やスキルを披露し、協力しあうことを目的に、2025年4月9日 - 11日の３日間開催します！</p>
</blockquote>
<p>開催場所: 立川ステージガーデン</p>
<img src="/images/2025/20250416b/f8129b9e-b619-4d24-a198-4b7166a5e696.jpeg" alt="" width="1200" height="900" loading="lazy">

<p>会場は、昭和記念公園が隣にあったり、自然と調和した商業施設も隣接していたり、とてもリラックスできるような場所でした。（立川ステージガーデンの椅子がとても座りやすく、セッションの視聴に集中できました。）</p>
<h3 id="try-Swift-Tokyo-2025のアプリ">try! Swift Tokyo 2025のアプリ</h3><p>try! Swift Tokyo 2025を楽しむための公式アプリがありました。</p>
<p>アプリ自体は、フルSwiftUI x TCAというモダンな構成で実装されているとのことなので、GitHubレポジトリから中身も見てみたいなと思います。</p>
<ul>
<li>AppStore: https://apps.apple.com/jp/app/try-tokyo-2025/id6479317240</li>
<li>GitHub: https://github.com/tryswift/try-swift-tokyo</li>
</ul>
<p>機能としては、スケジュールや会場までのルート、AI通訳などがありました。中でもAI翻訳がとても便利で、英語のセッションもリアルタイムで翻訳してくれていたので、とても助かりました。Flittoさんありがとうございました。</p>
<ul>
<li>AI通訳機能画面</li>
</ul>
<img src="/images/2025/20250416b/06e5ab60-6582-4ab4-957d-cfe1e5f62750.png" alt="" width="600" height="1299" loading="lazy">

<p>visionOS対応もしており、Apple Vision Proユーザーの方は、会場に表示されている壇上のスライドの上に、AI翻訳のウィンドウを表示させて見られてとても便利でしたという声もありました。（自分も体験してみたい。。。）</p>
<ul>
<li>日本語でのSwiftプログラミングのスピーカーFlipByBlink DevさんのXへ投稿より</li>
</ul>
<img src="/images/2025/20250416b/image_2.png" alt="image.png" width="600" height="483" loading="lazy">

<h2 id="良かった・役に立った・もっと理解したいと思ったセッション">良かった・役に立った・もっと理解したいと思ったセッション</h2><p>私が視聴して良かった・役に立った・もっと理解したいと思ったセッションをいくつか紹介します。</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>try! Swift Tokyo 2025の全セッションはtry!Swift TokyoのHPのタイムテーブルからご確認ください。</p>
</div></div>

<h3 id="iOS17-16-15での新機能-セッション動画はこちら">iOS17,16,15での新機能(セッション動画はこちら)</h3><p>タイトルの通り、iOS17,16,15それぞれで新しい利用できるようになったAPIを紹介する発表でした。<br>WWDCのセッション動画で利用できると知っても、使えるようになるまでにタイムラグがあったりするのでこのタイミングでiOS17,16,15のAPIをいくつかキャッチアップできたのはとても良かったです。具体的には以下のAPIの紹介がありました。</p>
<p><strong>紹介のあったiOS 15.0+で使えるAPI:</strong></p>
<ul>
<li><code>formatted()</code><ul>
<li>formatteddate:time:)</li>
<li>formatted(_:) → 表示したい桁数を指定できる。こちらは使いどころがありそう</li>
</ul>
</li>
<li><code>privacySensitive(_:)</code><ul>
<li>privacySensitive(_:) → 適用したViewを非表示にできる</li>
</ul>
</li>
<li><code>.toggleStyle()</code><ul>
<li>togglestyle(_:) → <code>.toggleStyle(.button)</code>などを使うことで組み込みのボタンの振る舞いが得られる</li>
</ul>
</li>
</ul>
<p><strong>紹介のあったiOS 16.0+で使えるAPI:</strong></p>
<ul>
<li><code>UIHostingConfiguration</code><ul>
<li>UIHostingConfiguration → これにより、SwiftUIを簡単にUIKitに入れ込むことができる</li>
</ul>
</li>
<li><code>TextField(_:,text:,axis: .vertical)</code><ul>
<li>init(_:text:axis:) → <code>TextField(_:,text:,axis: .vertical)</code>を使うことで、文字数が長くなったときに、テキストを途中で折り返し、テキストを全量表示できる</li>
</ul>
</li>
<li><code>Color.gradient</code><ul>
<li>gradient → デザイナーからの指定がないようなプロトタイプ作成のようなとりあえずグラデーションをかけたいときに便利</li>
</ul>
</li>
</ul>
<p><strong>紹介のあったiOS 17.0+で使えるAPI:</strong></p>
<ul>
<li><code>ContentUnavailableView.search</code><ul>
<li>ContentUnavailableView → .searchという静的プロパティで、検索結果がないときにViewを簡単に実装できる（ローカライズ対応や検索した文字をViewに簡単に組み込める）</li>
</ul>
</li>
<li><code>SubscriptionStoreView</code><ul>
<li>SubscriptionStoreView → 簡単にサブスクリプション用のViewを構築できる</li>
</ul>
</li>
<li><code>.scrolltargetbehavior</code><ul>
<li>scrolltargetbehavior(_:) → ページングなどにスクロールするときの振る舞いを指定できる（<code>.paged</code>や<code>.viewAligned</code>などがある）</li>
</ul>
</li>
<li><code>.containerrelativeframe()</code><ul>
<li>containerrelativeframe(_:alignment:) → このモディファイアで全体のサイズを計算したうえで、各子Viewのサイズを決定できるため、<code>GeometryReader</code>を使用せず済ませることができる（参考記事: 【SwiftUI】ScrollView内でframeを指定せず、画面にあったサイズに調整する）</li>
</ul>
</li>
<li><code>.visualEffect()</code><ul>
<li>visualEffect(_:) → <code>GeometryReader</code>を使用せずに、視覚効果を与えることができる</li>
</ul>
</li>
<li><code>.contentTransition(.numericText())</code><ul>
<li>contenttransition(_:)</li>
<li>numericText(countsDown:) → 数字のカウントダウン、アップのアニメーションをリッチなものにできる</li>
</ul>
</li>
</ul>
<h3 id="日本語でのSwiftプログラミング-セッション動画はこちら">日本語でのSwiftプログラミング(セッション動画はこちら)</h3><p>Xcode,Swiftにおいて、日本語の識別子は基本的にサポートしている。パフォーマンス面でも問題ないとのこと。<br>テスト関数名は、日本語にしてもいいかな何て思ったりもしました。</p>
<p>Swift Testingでは、XCTestのテスト関数で必須であったプレフィックス<code>test</code>が不要になるので、次のようなイメージで書くのも良いかもと思いました。</p>
<figure class="highlight diff"><table><tr><td class="code"><pre><span class="line"><span class="deletion">- @Test func foodTruckExists() &#123;</span></span><br><span class="line"><span class="addition">+ @Test func フードトラックが存在する() &#123;</span></span><br><span class="line">  // Test logic goes here.</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="⚡️-Swift-×-Android-Skipが切り拓くクロスプラットフォーム開発の未来-セッション動画はこちら">⚡️ Swift × Android: Skipが切り拓くクロスプラットフォーム開発の未来(セッション動画はこちら)</h3><p>SwiftとXcodeを使って、iOSとAndroidのアプリを作成できるツールSkipの実用性と今後クロスプラットフォーム対応の主流となるかについて、スピーカーが実際にアプリを作成したときの経験が紹介されていました。</p>
<ul>
<li>基本的にはSkipによって変換、生成されたコードでAndroidアプリのUIは期待通りになる。</li>
<li>2つのモードのうち、Native Modeを使うことをおすすめするとのこと。主な理由としては、もう一方のモードTranspile ModeではSwiftからKotlinに変換ができないAPIをがいくつかあるため。</li>
<li>既存で開発しているiOSアプリにSkipをそのまま適用は難しいとのこと。理由としては、iOSアプリで使用しているライブラリの一部がAndroidでサポートされていない。しかし、UIに関してはSwiftUIもそのままAndroidアプリに変換できるようにコンパイルできるようになってきている。</li>
<li>スピーカーは、Skipがクロスプラットフォームのメインストリームに数年でなるのではとのこと。</li>
</ul>
<p>skiptools&#x2F;skip-uiのSupported SwiftUIをみても多くのものがFull Supportになっているのも確認できました。これを見ても、純粋？なiOSエンジニアがクロスプラットフォーム対応するときには、Skipも有力候補に入ってきそうだなと感じました。<br>私自身は、Kotlinを書いたことがないので、SwiftのコードをそのままAndroid対応できるSkipはとても魅力的に映るために、時間をみつけてSkipを触ってみたいと思いました。</p>
<p>参考資料:</p>
<ul>
<li>Skip</li>
<li>https://github.com/skiptools</li>
<li>https://github.com/skiptools/skip-ui</li>
</ul>
<h3 id="Foreign-FunctionとMemory-APIとSwift-Java相互運用性-セッション動画はこちら">Foreign FunctionとMemory APIとSwift&#x2F;Java相互運用性(セッション動画はこちら)</h3><p>SwiftとJavaを相互に利用する方法について紹介がありました。<br>開発段階であるライブラリswiftlang&#x2F;swift-javaをAppleのエンジニアでContributorであるKonradさんがお話されていました。</p>
<p>swiftlang&#x2F;swift-javaでは、SwiftとJavaを相互運用するために次の２つのアプローチがあります。</p>
<ul>
<li><code>JavaKit</code></li>
<li>JavaのAPIを直接呼ぶことが可能にするSwiftライブラリ</li>
<li><code>jextract-swift</code></li>
<li><code>jextract</code>と類似したSwiftからJavaを効率的に呼び出すために使用するJavaソースを抽出するツール</li>
</ul>
<p>内容は私には理解が難しい部分が多々あったが、弊社ではJavaを使う案件が多いので、該当レポジトリの内容とセッションを見直したいと思いました。セッションの中で、UIをSwiftUIで残りの内部ロジック等をJavaに寄せることができるようなお話もあり、とても興味深いものでした。</p>
<h3 id="SwiftUI-を最適化するためのレンダーループの理解-セッション動画はこちら">SwiftUI を最適化するためのレンダーループの理解(セッション動画はこちら)</h3><p>SwiftUIを最適化するために、理解が必要なSwiftUIのレンダリングの仕組みや具体的な例を用いて表示遅延が起こる原因や解決策についての紹介がありました。</p>
<p>SwiftUIViewがどのようにレンダリングされているかという質問に対しては、レンダーループを理解することが近道です。</p>
<h4 id="レンダーループ-Render-loop">レンダーループ(Render loop)</h4><p>レンダーループ(Render loop)とは、アプリのUIがどのように更新され、画面に表示するかを管理する継続的なサイクルを指します。</p>
<p><strong>レンダーループのサイクル:</strong></p>
<ol>
<li>Event Phase<ul>
<li>何かしらのイベント&#x2F;状態変化を検知する</li>
</ul>
</li>
<li>View Update Phase<ul>
<li>Event Phaseでの変更を元に、新しいView階層を構築する</li>
<li>そのとき、以前のView階層と比較し、UIを更新するために必要な最小限の変更セットを作成する（この機能は、UIKitにはなく、SwiftUI独自のものであり、これによりViewの描画が最適化されている）</li>
</ul>
</li>
<li>Render phase<ul>
<li>SwiftUIが新しいView階層をGPUで処理可能な形式に変換する（これには、色、形、画像などの視覚に関するプロパティが含まれる）</li>
</ul>
</li>
<li>Display Buffer<ul>
<li>Render PhaseでDisplay Bufferが作成される</li>
</ul>
</li>
<li>Display<ul>
<li>Diplay Bufferのピクセルデータをユーザー用の最終的な視覚情報に変換する</li>
</ul>
</li>
</ol>
<h4 id="サンプルプロジェクトによる例示">サンプルプロジェクトによる例示</h4><p>サンプルコード: https://github.com/pradnya-nikam/trySwiftTokyoPrad2025</p>
<p>サンプルコードの雲をたくさん描画するViewでは、動く雲の数が多くなるにつれて動きもっさりしてきます。描画時のCPUの状況を見ると、徐々にCPUの使用率が上昇していくことが紹介されていました。これはRender Phaseですべての雲の位置を計算、雲の形を描画し、視覚効果を適用しているためです。このため、Render Phaseに割り当てられているフレーム内で処理がしきれなくなってしまい、ラグが発生してしまいます。</p>
<p>そこで、<code>.drawingGroup()</code>を適用して、ラグを解消します。</p>
<figure class="highlight diff_swift"><table><tbody><tr><td class="code"><pre><span class="line">  <span>CloudView</span>()</span><br><span class="line">      .drawingGroup() <span class="comment">// これを加えるだけ</span></span><br></pre></td></tr></tbody></table></figure>

<p>drawinggroup(opaque:colormode:)</p>
<p>このメソッドを使うことで、画面上には見えないオフスクリーン上でMetal APIを使用して、1つのイメージにまとめて描画することでパフォーマンスを改善させています。<br>実際に、CPUの使用率も、適用前の112%から33%ほどまで下がっていました。</p>
<p><strong>Cherry Blossoms! App:</strong></p>
<p>反対に、桜が散っていくアニメーションがあるViewでは、<code>.drawingGroup()</code>を適用することで逆に、動きが遅くなり、不安定になってしまいました。この理由はこのアプリの花びらは削除と作成を繰り返すため、このメソッドで解決するようなシチュエーションではないとのことです。</p>
<p>このように、<code>drawingGroup()</code>を使わない方よい例は以下の4つです。</p>
<ol>
<li>Views with Dynaminc Content → Viewのコンテンツが頻繁に変更されるもの</li>
<li>Text Views → テキストを表示するときのレンダリングは複数なレイアウトとグリフ管理があるため</li>
<li>Views with interactions → 予期しない変更が生じる可能性がある</li>
<li>Special Visual effect like some blending modes maynot work properly</li>
</ol>
<p>本セッション内容は、SwiftUIを普段使っている私にはとても勉強になりました。レンダーループについては、サンプルコードを手元で動かしつつ、Instruments等を使ってパフォーマンスに関して色々実際に検証してみたいと思いました。</p>
<p>参考資料:</p>
<ul>
<li>https://developer.apple.com/videos/play/tech-talks/10855/</li>
<li>https://developer.apple.com/videos/play/wwdc2018/219/</li>
<li>https://developer.apple.com/videos/play/wwdc2020/10077/</li>
<li>https://developer.apple.com/videos/play/tech-talks/10857/?time=1091</li>
<li>(My own talk that talks about Rendering on UIKit) https://www.youtube.com/watch?v=gbAjrPqFLUY</li>
</ul>
<h2 id="さいごに">さいごに</h2><p>今回は一般参加で、try! Swift Tokyo2025に参加しました!</p>
<p>次回は別の形(スタッフやスピーカーなど)で参加してtry! Swiftのコミュニティの発展に貢献したいと感じました。</p>
<p>英語話者が多いイベントということで、久しぶりに英語を聞いたり、話したりしましたが学生時代より聞き取れなかったり、言葉が出ないということがあったので英会話をまた始めたいなと思うきっかけになりました。</p>
<p>セッションでは、技術的な知識だけではなく、取り組む姿勢や心持ちみたいなことも話されている方でいて、様々な面で良い刺激を受けることができました。来年の開催も楽しみにしています！</p>
<img src="/images/2025/20250416b/a03209ce-1e32-4b7d-b736-001fb3ec3a2a.jpeg" alt="" width="1200" height="634" loading="lazy">



<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">初めてtry! Swift Tokyo 2025に参加してきました！参加報告として、興味深かったセッションや参加して感じたことなどを共有します。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="Xcode" scheme="https://future-architect.github.io/tags/Xcode/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
    <category term="参加レポート" scheme="https://future-architect.github.io/tags/%E5%8F%82%E5%8A%A0%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
  </entry>
  <entry>
    <title>Swift6移行に向けて、Isolation domainとSendableを理解する</title>
    <link href="https://future-architect.github.io/articles/20241208a/"/>
    <id>https://future-architect.github.io/articles/20241208a/</id>
    <published>2024-12-07T15:00:00.000Z</published>
    <updated>2024-12-07T15:00:00.000Z</updated>
    <author><name>橋本竜我</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20241208a/image.png" alt="" width="1200" height="416">

<div class="note-container note-info"><span class="note-icon"></span><div>

<p>本記事は、Swift Advent Calendar 2024の8日目です。<br>7日目は、@hinakkoさんのSplit Viewを考慮したSize Classを使用したiPad対応です。</p>
</div></div>

<p>HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>の橋本です。</p>
<p>先日参加したSwiftZoomin#20の内容から、Swift6移行に向けて理解が必要なSwift Concurrencyの重要な概念について簡単にまとめました。</p>
<p>SwiftZoomin#20の動画は、次のリンク先からYouTube上で視聴可能です。</p>
<ul>
<li>感覚的に理解するConcurrency: Swift 6はIsolationとSendableを用いてどのようにデータ競合を防止するか</li>
</ul>
<h2 id="Swift6移行に向けて、重要な概念3つ">Swift6移行に向けて、重要な概念3つ</h2><p>以下３つの概念についてまとめいきます。</p>
<ul>
<li>Isolation domain</li>
<li>Isolation boundary</li>
<li>Sendable</li>
</ul>
<h3 id="Isolation-domain">Isolation domain</h3><p><code>隔離=Isolation</code>する領域のことを<code>Isolation domain</code>と呼びます。<br><code>Isolation domain</code>の重要な特性は、<strong>一つのIsolation domainのなかでは同時に一つの処理しか実行されない</strong>ことです（＝1つのIsolation domainの中で並行実行されることはないことと同義）</p>
<p>参考</p>
<ul>
<li>Isolation Domains | Migrating to Swift 6</li>
</ul>
<h4 id="actor">actor</h4><p><code>actor</code>というのは、1つの領域で隔離(Isolation)することでデータ競合を防いでいます。</p>
<p>最も馴染み深いのは、<code>MainActor</code>の<code>Isolation domain</code>であり、<code>actor</code>のインスタンスに紐づいたIsolation domainも存在します。</p>
<p><code>MainActor</code>の<code>Isolation domain</code>は<code>Main Actor</code>で保護された領域を表します。<br><code>Actor</code>の場合は、<code>Actor</code>のインスタンスごとに<code>Isolation domain</code>をもち、同じ<code>actor</code>でもインスタンスが異なれば、それぞれ異なる<code>Isolation domain</code>を持ちます。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">actor</span> <span class="title class_">Counter</span> &#123;<span class="operator">...</span>&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> a <span class="operator">=</span> <span class="type">Counter</span>() <span class="comment">// aのIsolation domain、aとbは別のIsolation domainである。</span></span><br><span class="line"><span class="keyword">let</span> b <span class="operator">=</span> <span class="type">Counter</span>() <span class="comment">// bのIsolation domain、aとbは別のIsolation domainである。</span></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>参考</p>
<ul>
<li>Actor | Apple Developer Documentation</li>
</ul>
<h3 id="Isolation-boundary">Isolation boundary</h3><p><code>Isolation domain</code>が複数存在しているときのそれぞれの<code>Isolation domain</code>の間の境界のことを<code>Isolation boundary</code>と呼びます。</p>
<p>アプリにおいては、それぞれの<code>Isolation domain</code>間で<code>Isolation boundary</code>を超えて情報のやり取りをしないといけない場面が多々あります。</p>
<p>この<code>Isolation boundary</code>を超えて値を受け渡すために、次の<code>Sendable</code>が必要になってきます。</p>
<h3 id="Sendable">Sendable</h3><p><strong>Sendable 並行にアクセスされても安全な型だけが準拠できるプロトコル</strong><br><code>Sendable</code>の特徴は、<code>Sendable</code>に準拠した型の値だけが<code>Isolation boudnary</code>を超えられるようになります。<br>つまり、<code>Sendable</code>を使うことで実行時にデータ競合が起こらないことをコンパイル時にチェックできるように、型の問題とすることで実現しています。</p>
<h4 id="non-Sendable">non-Sendable</h4><p><code>Sendable</code>に対して、<code>non-Sendable</code>な値は、<code>Isolation boundary</code>を超えることができません。</p>
<ul>
<li>Sendable | Apple Developer Documentation</li>
<li>Sendable Types | Migrating to Swift 6</li>
<li>Sendable Types | The Swift Programming Language</li>
</ul>
<h3 id="Sendableを体感する">Sendableを体感する</h3><img src="/images/2024/20241208a/Sendable.png" alt="" width="1031" height="685" loading="lazy">

<p>上記の<code>Box</code>クラスをSendableに準拠させて、並行にアクセス可能にさせてみる。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">Box</span>: <span class="title class_ inherited__">Sendable</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> value: <span class="type">Int</span> <span class="operator">=</span> <span class="number">0</span> <span class="comment">// ここでエラー(Stored property &#x27;value&#x27; of &#x27;Sendable&#x27;-conforming class &#x27;Box&#x27; is mutable)</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>そうすると、次のように<code>Sendable</code>に準拠すること自体がエラーであることがわかります。<br><code>Box</code>型のプロパティ<code>Value</code>が可変状態のため、並行にアクセスすると危険である旨のメッセージが出てきます。</p>
<figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">Stored property &#x27;value&#x27; of &#x27;Sendable&#x27;-conforming class &#x27;Box&#x27; is mutable</span><br></pre></td></tr></table></figure>

<p>ここで、<code>var</code>ではなく、<code>let</code>だったらどうなるか考えてみます。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">Box</span>: <span class="title class_ inherited__">Sendable</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> value: <span class="type">Int</span> <span class="operator">=</span> <span class="number">0</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><code>let</code>にすることで、<code>Box</code>クラスのエラーは解消されます。</p>
<p><code>value</code>プロパティが定数になるため、<code>run()</code>メソッド内で<code>box.balue = -1</code>で書き込みをしていた箇所がコンパイルエラーになります。</p>
<p>この部分を読み込むだけの<code>print(box.value)</code>とすれば、データ競合を起こさない安全なコードとすることができます。データ競合は、並行にアクセスする少なくとも１つで書き換えが起こっているときに生じるものであるため、今回のように読み込むだけであれば、データ競合の危険性はありません。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">func</span> <span class="title function_">run</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> box: <span class="type">Box</span> <span class="operator">=</span> .<span class="keyword">init</span>()</span><br><span class="line"></span><br><span class="line">    <span class="type">Task</span> &#123;</span><br><span class="line">        <span class="comment">// box.value = -1 // Cannot assign to property: &#x27;value&#x27; is a &#x27;let&#x27; constant</span></span><br><span class="line">        <span class="built_in">print</span>(box.value)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">Task</span> &#123;</span><br><span class="line">        <span class="built_in">print</span>(box.value)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="Isolation-boundaryを体感する">Isolation boundaryを体感する</h3><p><code>Sendable</code>に準拠していない<code>non-Sendable</code>な<code>Box</code>クラスが<code>Isolation boundary</code>を超えるとコンパイルエラーが出る例を示します。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">Box</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> value: <span class="type">Int</span> <span class="operator">=</span> <span class="number">0</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">actor</span> <span class="title class_">A</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> box: <span class="type">Box</span>?</span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">setBox</span>(<span class="keyword">_</span> <span class="params">box</span>: <span class="type">Box</span>) &#123;</span><br><span class="line">        <span class="keyword">self</span>.box <span class="operator">=</span> box</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">func</span> <span class="title function_">run</span>() <span class="keyword">async</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> box: <span class="type">Box</span> <span class="operator">=</span> .<span class="keyword">init</span>()</span><br><span class="line">    <span class="keyword">let</span> a: <span class="type">A</span> <span class="operator">=</span> .<span class="keyword">init</span>() <span class="comment">// actor A のインスタンスa</span></span><br><span class="line"></span><br><span class="line">   <span class="keyword">await</span> a.setBox(box) <span class="comment">// actor A のインスタンスaにboxを渡す　⇐ boxがIsolation bounadaryを超えて、actor AのインスタンスaのIsolation domainに入る。</span></span><br><span class="line"><span class="comment">// ここで次のエラーメッセージが出る。</span></span><br><span class="line"><span class="comment">// Sending &#x27;box&#x27; risks causing data races</span></span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(box.value)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><code>Sendable</code>に準拠していない<code>Box</code>クラスの<code>box</code>インスタンスが<code>Isolaton boudary</code>を超えたため、このエラーがでてきます。</p>
<p>このように、<code>Isolaton boudary</code>が存在していることを体験できました。</p>
<h2 id="おわりに">おわりに</h2><p>SwiftZoomin#20（感覚的に理解するConcurrency: Swift 6はIsolationとSendableを用いてどのようにデータ競合を防止するか）動画前半の内容について私なりにまとめさせていただきました（@koherさん、大変わかりやすく説明いただきありがとうございました）。</p>
<p>Swift6への移行は、コンパイル時にデータ競合が起こる可能性のある箇所を事前につぶすことができるので、アプリの品質向上に直結するため、ビジネス的にもとても有用なものだと思っております。</p>
<p>本記事がSwift6移行に向けて、少しでも参考になれば幸いです。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">SwiftZoomin#20の内容から、Swift6移行に向けて理解が必要なSwift Concurrencyの重要な概念について簡単にまとめました。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
  </entry>
  <entry>
    <title>iOSDC Japan 2024に参加してきました！</title>
    <link href="https://future-architect.github.io/articles/20240912a/"/>
    <id>https://future-architect.github.io/articles/20240912a/</id>
    <published>2024-09-11T15:00:00.000Z</published>
    <updated>2024-09-11T15:00:00.000Z</updated>
    <author><name>橋本竜我</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240912a/S__104128514_0.jpg" alt="" width="1200" height="900">

<h2 id="はじめに">はじめに</h2><p>HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>の橋本です。</p>
<p>初めてiOSDC Japan 2024にオフライン参加してきました！</p>
<p>参加報告として、いくつかのセッションとその内容を紹介したいと思います！</p>
<h2 id="iOSDC-Japanとは">iOSDC Japanとは</h2><p>国内を中心に多くのiOS開発に関わる多くの人々が集まるiOSのDeveloper Conference（開発者会議）です。</p>
<p>公式サイトの説明は次のようになっており、技術的な話題から開発者同士の繋がりを持てるパーティや懇親会などの様々なイベントがあります。</p>
<blockquote>
<p>iOSDC Japan 2024はiOS関連技術をコアのテーマとしたソフトウェア技術者のためのカンファレンスです。今年もリアル会場とオンライン配信のハイブリッド開催を予定しています。<br>日本中、世界中から公募した知的好奇心を刺激するトークの他にも、パンフレットに掲載された技術記事、自分のペースでゆっくり参加できるポスターセッション、気持ちを盛り上げるネイルアートやフェイスペインティング、オープニングパーティ &amp; 懇親会などなど、初心者から上級者まで楽しめるコンテンツがみなさんを待っています。<br>8月22日（木）〜 8月24日（土）はお祭りです！お楽しみに！！</p>
</blockquote>
<p>公式HP: https://iosdc.jp/2024/</p>
<h2 id="特に気になったセッションの紹介">特に気になったセッションの紹介</h2><p>視聴したセッションの中で、印象に残ったものの紹介と感想を記載します。</p>
<ul>
<li>座談会 「Strict ConcurrencyとSwift 6が開く新時代: 私たちはどう生きるか？」 by shiz</li>
<li>MV State PatternでSwiftUIを受け入れる by Hiroma Tsuchiya</li>
<li>Swift 6のTyped throwsとSwiftにおけるエラーハンドリングの全体像を学ぶ by koher</li>
</ul>
<h3 id="座談会-「Strict-ConcurrencyとSwift-6が開く新時代-私たちはどう生きるか？」-by-shiz">座談会 「Strict ConcurrencyとSwift 6が開く新時代: 私たちはどう生きるか？」 by shiz</h3><p>発表資料: https://speakerdeck.com/shiz/zuo-tan-hui-strict-concurrencytoswift-6gakai-kuxin-shi-dai-si-tatihadousheng-kiruka</p>
<h4 id="発表概要と感想">発表概要と感想</h4><p>iOS界隈の強強エンジニアの方々の座談会で、Swift6への移行に向けてのStrict Concurrency対応についてお話されていました。</p>
<p>Swift 6では、Actorを使ったデータ競合防止の仕組みがさらに強化されます。特に、Actor間で扱うデータの安全性がコンパイラで厳密にチェックされるため、既存のコードがコンパイルエラーとなる可能性があります。これにより、並行処理の安全性が一層向上しますが、我々はコードの修正が必要になってくるケースがあります。</p>
<p><strong>『いつまでにStrict Concurrency 対応をするべきか。』</strong><br>この問いに答えとしては、Xcode 16でもSwift5言語モードを利用可能であるため、ひとまずは直ぐにコンパイルエラーまみれになって急に動かなくなるということは避けられそうです。</p>
<p>この座談会でお聞きして、Swift6への移行に向けて以下の知識をしっかりと理解する必要があると感じました。</p>
<ul>
<li>Swift Concurrency<ul>
<li>Sendable</li>
<li>Actor</li>
<li>isolation</li>
</ul>
</li>
</ul>
<p>私や開発チームとしては、Complete Strict Concurrency Checkを使って、まずは既存のコードでSwift6にあげるとどこがコンパイルエラーになるかを洗い出しをするところから作戦を立てて行きたいと思います。</p>
<p>本セッション内で紹介のあった事例別！Strict Concurrency対応方法やSwiftコミュニティのSwift6へのマイグレーションガイド（Migrating to Swift 6）を参考にしたいです。また、日本語訳版についてもSwift 6への移行も既にまとめて頂いており、めちゃくちゃありがたいです。次のApple公式ドキュメントやサンプルコードも参考にしたいと思います。</p>
<ul>
<li>Adopting strict concurrency in Swift 6 apps</li>
<li>Updating an app to use strict concurrency | Apple Developer Documentation</li>
</ul>
<h3 id="MV-State-PatternでSwiftUIを受け入れる-by-Hiroma-Tsuchiya">MV State PatternでSwiftUIを受け入れる by Hiroma Tsuchiya</h3><h4 id="発表概要と感想-1">発表概要と感想</h4><p>発表内容は、MV State Patternというアーキテクチャに関する説明と、採用理由とその実装方法について紹介しているセッションでした。</p>
<h4 id="MVVM-x-SwiftUIの組み合わせで感じた違和感">MVVM x SwiftUIの組み合わせで感じた違和感</h4><p>発表者の開発チームでは、MVVMとSwiftUIでの開発経験がありその中でいくつかの違和感や課題を感じていたとのことです。<br>具体的に、感じていた違和感は次の２つがあったとのことです。</p>
<ol>
<li>ViewModelを精密に構築するほど、SwiftUIフレームワークの機能(各種プロパティラッパー)を捨てるというジレンマがあった</li>
<li>データバインディングの機能はSwiftUIにおいてすでにViewに組み込まれているため、UIKit時代の役割のままViewModelを持ち込む必要性に疑問視</li>
</ol>
<p>2.に関して、発表者の話にあったようにSwiftUIはそもそも@State・@Binding・@ObservedObjectなどを使うことで、ビューとデータの間に自動的な連携が行われれる機能があります。状態が変わればUIが更新され、UIでユーザーが入力した内容がデータに反映される、という関係が自動的に保たれているので、ユースケースによってはViewModelの必要性は低くなることには納得しました。</p>
<p>また、Apple公式ドキュメントでも、次のように言及されています。</p>
<p>https://developer.apple.com/documentation/swiftui/monitoring-model-data-changes-in-your-app</p>
<blockquote>
<p>Traditionally, you use a view controller to move data back and forth between the model and the UI, but SwiftUI handles most of this synchronization for you. To update views when data changes, you make your data model classes observable objects, publish their properties, and declare instances of them using property wrappers.</p>
</blockquote>
<h4 id="MV-State-Patternとは">MV State Patternとは</h4><p>Appleの公式ドキュメントやサンプルコードで使われていたアーキテクチャに対して、開発者コミュニティがMV State Patternと名前をつけてあげたアーキテクチャみたいです。<br>このMVState Patternのデータフローパターンのコンセプトは次のようになっています。</p>
<ul>
<li>SwiftUIがどのように、StateとViewを結びつけているかという構造と全く同じです。具体的には次のような特徴を持ちます。<ul>
<li>ユーザーがアクションを実行することで状態が更新され、再レンダリングを起こす</li>
<li>単方向のループのように動作し、状態が更新されればいつでも新しいバージョンのViewが作られる</li>
<li>ユーザーの入力に限定されず、タイマーや通知などの外部からの状態変更もループに組み入れられる</li>
</ul>
</li>
</ul>
<p>これは、WWDC2019のData Flow Through SwiftUIのセッションで登場した図と同じ内容です。</p>
<img src="/images/2024/20240912a/スクリーンショット_2024-09-07_21.40.35.png" alt="" width="1200" height="685" loading="lazy">

<p>https://developer.apple.com/videos/play/wwdc2019/226/</p>
<p>Viewにバリデーションや特定のコンポーネントにのための処理を含み、MVVMでいうViewModelの役割も持ちます。しかし、ViewModelで書くような処理をすべて、Viewに持たせるため、コードが長くなる傾向がありました。</p>
<p><strong>Viewが大きくなることに対する対応策</strong></p>
<ol>
<li><p>MARKの活用</p>
<ul>
<li><p>Main View, SubView, ViewLogicを次のようにコード上に目印として記載する。（ミニマップからMARKの箇所に飛べるのも便利。）</p>
  <figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// MARK: MainView</span></span><br><span class="line"><span class="operator">...</span></span><br><span class="line"><span class="comment">// MARK: SubView</span></span><br><span class="line"><span class="operator">...</span></span><br><span class="line"><span class="comment">// MARK: ViewLogic</span></span><br><span class="line"><span class="operator">...</span></span><br><span class="line"></span><br></pre></td></tr></table></figure></li>
</ul>
</li>
<li><p>ViewからViewLogicへ切り出す</p>
<ul>
<li><p>メリット: Viewプロトコル準拠ではなくなるため、テストコードも容易にかける</p>
  <figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">SomeViewLogic</span> &#123;</span><br><span class="line">    <span class="operator">...</span></span><br><span class="line">    <span class="keyword">init</span>(<span class="operator">...</span>) &#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> title: <span class="type">String</span>? &#123;</span><br><span class="line">    <span class="operator">...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li>
</ul>
</li>
</ol>
<h4 id="MV-State-Patternのメリット・デメリット">MV State Patternのメリット・デメリット</h4><p>主なメリットは、SwiftUIのシンプルさに従ったアーキテクチャであることによる学習コストの低さとシンプルさであり、<br>デメリットは、(MVVMと比べると、)プレゼンテーション層の関心の分離が甘く、境界が曖昧になりやすいこととの紹介がありました。つまり、Fat Viewに陥る可能性が高いといえます。先程紹介したように、この問題には、MARKの活用とViewLogicへの切り出しを行い、コードをクリーンに保つ必要があります。</p>
<p>Viewが大きくなることに対する対応策は、アーキテクチャの種類にかかわらず、Fat-viewに対応する方法として有効であるため、私の関わるアプリにおいても参考にしたいと思いました。</p>
<h3 id="Swift-6のTyped-throwsとSwiftにおけるエラーハンドリングの全体像を学ぶ-by-koher">Swift 6のTyped throwsとSwiftにおけるエラーハンドリングの全体像を学ぶ by koher</h3><p>発表資料: https://speakerdeck.com/ridwy/kong-jian-bideohuomatuto-mv-hevcgaxie-sicuo-rushi-jie</p>
<h4 id="発表概要と感想-2">発表概要と感想</h4><p>こちらのセッションでは、Swift 6から導入されるTyped throwsとSwiftにおけるエラーハンドリングの全体像を知ることができました。</p>
<p>ここでは、発表内容から大変わかりやすかったTyped Throwsが解決する問題とその解決方法を簡単に紹介したいと思います。</p>
<h4 id="Untyped-throwsの問題">Untyped throwsの問題</h4><p>これまでのthrowsのUntyped throwsの問題点を解決する方法として、Typed throwsが導入されます。</p>
<p>次のような<code>parseInt</code>を関数をエラーハンドリングする例を考えます。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Stringを受け取って、Intを返す関数</span></span><br><span class="line"><span class="keyword">func</span> <span class="title function_">parseInt</span>(<span class="keyword">_</span> <span class="params">value</span>: <span class="type">String</span>) <span class="keyword">throws</span> -&gt; int</span><br><span class="line">    <span class="keyword">if</span> value.isEmpty &#123;<span class="keyword">throw</span> <span class="type">ParseError</span>.emptyValue &#125;</span><br><span class="line">        <span class="keyword">guard</span> <span class="keyword">let</span> number <span class="operator">=</span> <span class="type">Int</span>(value) <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> value.contains(<span class="regexp">/^[-]\d+$/</span>) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="type">ParseError</span>.outOfBounds(value)</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="type">ParseError</span>.illegalFormat(value)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    <span class="keyword">return</span> number</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">enum</span> <span class="title class_">ParseError</span>: <span class="title class_ inherited__">Error</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> emptyValue</span><br><span class="line">    <span class="keyword">case</span> illegalFormat(<span class="type">String</span>)</span><br><span class="line">    <span class="keyword">case</span> outOfBounds(<span class="type">String</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>上記の関数を<code>do-catch</code>を使ってハンドリングすると次のようになります。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ハンドリングする側</span></span><br><span class="line"><span class="keyword">do</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> number: <span class="type">Int</span> <span class="operator">=</span> <span class="keyword">try</span> parseInt(string) <span class="comment">//　ParseErrorしか返さない関数</span></span><br><span class="line">    <span class="built_in">print</span>(number)</span><br><span class="line">&#125; <span class="keyword">catch</span> &#123;</span><br><span class="line">    <span class="built_in">print</span>(error) <span class="comment">// 暗黙のerrorはany Error型になってしまう。</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>この書き方だと、<code>catch</code>節の中の<code>error</code>は<code>any Error</code>型となってしまいます。<br>次に、ParseError型でハンドリングするように書き直します。</p>
<p>ParserErrorハンドリングできるように、変更</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="keyword">do</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> number: <span class="type">Int</span> <span class="operator">=</span> <span class="keyword">try</span> parseInt(string) <span class="comment">//　ParseErrorしか返さない関数</span></span><br><span class="line">    <span class="built_in">print</span>(number)</span><br><span class="line">&#125; <span class="keyword">catch</span> <span class="keyword">let</span> error <span class="keyword">as</span> <span class="type">ParseError</span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> error &#123; <span class="comment">// ParseErrorを返す</span></span><br><span class="line">    <span class="built_in">print</span>(error) <span class="comment">// any Error型になってしまう。</span></span><br><span class="line">    <span class="keyword">case</span> .emptyValue: <span class="operator">...</span></span><br><span class="line">    <span class="keyword">case</span> .illegalFormat(<span class="keyword">let</span> value): <span class="operator">...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>しかし、このコードだとcatchが網羅的ではないよと次のエラーがでます。</p>
<blockquote>
<p>　Errors thrown from here are not handled because the enclosing catch is not exhaustive</p>
</blockquote>
<p><code>parseInt()</code>は<code>ParseError</code>しか<code>throw</code>しないが、コンパイラからはそれがわからないため、あらゆるエラーをスローすると判断します。<code>catch</code>節で<code>ParseError</code>をハンドルしているが、コンパイラ側からはany <code>Error</code>が投げられると判断され、網羅的ではないよとエラーがでます。これのワークアラウンドとして次の方法があります。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ParserErrorハンドリングするように`preconditionFailure()`を追加するように、変更します。</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">do</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> number: <span class="type">Int</span> <span class="operator">=</span> <span class="keyword">try</span> parseInt(string) <span class="comment">//　ParseErrorしか返さない関数</span></span><br><span class="line">    <span class="built_in">print</span>(number)</span><br><span class="line">&#125; <span class="keyword">catch</span> <span class="keyword">let</span> error <span class="keyword">as</span> <span class="type">ParseError</span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> error &#123; <span class="comment">// ParseErrorを返す</span></span><br><span class="line">    <span class="built_in">print</span>(error) <span class="comment">// any Error型になってしまう。</span></span><br><span class="line">    <span class="keyword">case</span> .emptyValue: <span class="operator">...</span></span><br><span class="line">    <span class="keyword">case</span> .illegalFormat(<span class="keyword">let</span> value): <span class="operator">...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125; <span class="keyword">catch</span> &#123;</span><br><span class="line">    <span class="built_in">preconditionFailure</span>(<span class="string">&quot;Never reaches here.&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>これがUntyped Throwsの問題であり、これを解決するのがTyped Throwsになります。</p>
<h4 id="Typed-throwsによる上記の問題の解決方法">Typed throwsによる上記の問題の解決方法</h4><p>Typed throwsでは、さきほどの関数<code>parseInt</code>を次のようにして、エラーの型を指定できるようになります。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">func</span> <span class="title function_">parseInt</span>(<span class="keyword">_</span> <span class="params">value</span>: <span class="type">String</span>) <span class="keyword">throws</span>(<span class="type">ParseError</span>) -&gt; <span class="type">Int</span></span><br></pre></td></tr></table></figure>

<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">do</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> number <span class="operator">=</span> <span class="keyword">try</span> parseInt(string)</span><br><span class="line">    <span class="built_in">print</span>(number)</span><br><span class="line">&#125; <span class="keyword">catch</span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> error &#123; <span class="comment">// typed throwsにより、any Error型ではなく、ParseError型を解釈できるようになる。</span></span><br><span class="line">    <span class="operator">...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>最後に、Typed throwsはswift-evolution上で安易に使うべきではないと繰り返し記載されているので、本当に使う必要があるのかをよく考える必要があることを学びました。swift-evolutionのwhen to use typed throwsの内容を確認してみたいと思います。</p>
<p>本セッションでは、安易に使うべきではない理由として次の２つが挙げられていました。</p>
<ol>
<li>エラーの型は変化しやすい</li>
<li>エラーが網羅的にハンドリングされることは少ない</li>
</ol>
<p>Typed throwsの詳細を確認したい方は、次のswift-evolutionのSE-0413: Typed throwsを確認してみてください。</p>
<p>また、Typed throwsのお話だけではなく、後半のエラーハンドリングの全体像のお話は大変参考になりました。<br>エラーハンドリングの考え方が詳しく記されている次のドキュメントをもとにお話をされていましたので、時間があるときに目を通しておきたいと思います。このドキュメントは、swift2がリリースされたときの公開されたもののようです。</p>
<ul>
<li>Error Handling Rationale and Proposal</li>
</ul>
<p>エラー処理を書くときに全体像を一度思い出して、この書き方で良いだろうかと自問自答したいなと思いました。koherさんがiOSDC2017年のセッションiOSDC Japan 2017 09&#x2F;16 Track B 11:20 &#x2F; 具体例とクイズで学ぶ、Swiftの4種類のエラーの使い分け &#x2F; Yuta Koshizawaでも同じようにエラーハンドリングの全体像のお話をされていたので、こちらも合わせて視聴することをおすすめします。</p>
<h2 id="上記以外で面白かった、参考になったセッション">上記以外で面白かった、参考になったセッション</h2><h3 id="Appleの新しいプライバシー要件対応：ノーコード-アプリプラットフォームの実践事例-by-Nao-RandD-ナオランド">Appleの新しいプライバシー要件対応：ノーコード アプリプラットフォームの実践事例 by Nao-RandD &#x2F; ナオランド</h3><p>私自身、担当している案件でプライバシーマニフェスト対応を進めており、再度プライバシーマニフェストの対応方法について歴史や今後についてまで紹介されており大変参考になりました。</p>
<p>発表資料: https://speakerdeck.com/nao_randd/applenoxin-siipuraibasiyao-jian-dui-ying-nokodoapuri-puratutohuomunoshi-jian-shi-li</p>
<p>ヤプリさんのテックブロクにもプライバシーマニフェスト対応について大変詳しく記載されているので、これからプライバシーマニフェスト対応を進める方にはとても参考になると思います。</p>
<ul>
<li>ヤプリiOSエンジニアの新プライバシー要件対応① | 時系列で振り返り、関連用語を理解する</li>
<li>ヤプリiOSエンジニアの新プライバシー要件対応② | 実際に取り組んだ内容を大公開！</li>
<li>ヤプリiOSエンジニアの新プライバシー要件対応③ | ノーコードアプリプラットフォームの運用体制も大公開！</li>
</ul>
<p>現状は、プライバシーマニフェストのApple側のレビューはそこまで厳しくないようなので、今後段階的に厳しくなっていく可能性もあると思うのでプライバシーマニフェストに関する動向は引き続きウォッチしていきたいと思います。</p>
<h3 id="空間ビデオフォーマット、MV-HEVCが写し撮る世界-by-なめき-ちはる">空間ビデオフォーマット、MV-HEVCが写し撮る世界 by なめき　ちはる</h3><p>Apple Vision Proでの空間ビデオフォーマットについてやどうやって立体視を実現しているかなど、人の目の仕組みにも触れていてとても興味深いセッションでした。</p>
<ul>
<li>発表資料: https://speakerdeck.com/ridwy/kong-jian-bideohuomatuto-mv-hevcgaxie-sicuo-rushi-jie</li>
</ul>
<h3 id="これからの時代の新標準！Apple製テストフレームワーク「SwiftTesting」への移行とトラブルシューティング">これからの時代の新標準！Apple製テストフレームワーク「SwiftTesting」への移行とトラブルシューティング</h3><p>新しいテストフレームワークSwiftTestingの概要とXCTestからの移行作業についてお話されていて、大変参考になるセッションでした。</p>
<p>移行作業の全体像だったり、XCTestをSwiftTestingの記法に置き換えてくれるツール(giginet&#x2F;swift-testing-revolutionary)も知れたので、大変有意義な5分間でした。</p>
<p>発表資料: https://speakerdeck.com/uetyo/korekaranoshi-dai-noxin-biao-zhun-swifttestinghenoyi-xing-totoraburusiyuteingu</p>
<h3 id="医療アプリ開発の最前線-安全性と生産性の両立への挑戦-by-Shogo-Yoshida">医療アプリ開発の最前線 - 安全性と生産性の両立への挑戦 by Shogo Yoshida</h3><p>医療アプリ開発を通じて得た安全性と生産性の両立に関する知見を共有頂いたありがたいセッションでした。</p>
<p>具体的には、次のような知見が紹介されました。</p>
<ul>
<li>継続的なリファクタリングに取り組みつつ、Architecture Decision Record(ADR)を残すこと</li>
<li>トランクベース開発を採用</li>
<li>OpenAPI Generatorを使ったAPI仕様に合わせたリクエスト、モデルのコードの自動生成</li>
<li>Renovateを使用してライブラリの自動アップデートの管理</li>
<li>テスト自動化サービスMagicPodの活用</li>
</ul>
<p>やはり、品質を高めつつ、生産性を高める方法は自動化することがとても大事だなとしみじみ感じました。</p>
<p>発表資料: https://speakerdeck.com/medley/yi-liao-apurikai-fa-nozui-qian-xian-an-quan-xing-tosheng-chan-xing-noliang-li-henotiao-zhan</p>
<h2 id="さいごに">さいごに</h2><p>はじめてオフライン参加した今年のiOSDCは、現地参加ならではのコンテンツを楽しめたり、Xで会話したことがあるiOSエンジニアの方にお会いしてお話ができたりできたと、とても有意義な時間でした。</p>
<p>また、他の企業さんのプロダクトのコードをアンカンファレンスで、実際に見させて頂く機会をもらったり、貴重な経験ができました。</p>
<p>2025年は私自身なにかしらの形でiOSDC Japanに貢献できればと思っています。今から来年のiOSDC Japan 2025が楽しみです〜！</p>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3506730/c009b2b3-66e2-e4f8-160e-e8eb1c4527b8.jpeg" alt="" width="554" height="739" loading="lazy">

<h2 id="参考">参考</h2><ul>
<li>iOSDC Japan 2024</li>
<li>座談会 「Strict ConcurrencyとSwift 6が開く新時代: 私たちはどう生きるか？」 by shiz</li>
<li>座談会 「Strict ConcurrencyとSwift 6が開く新時代: 私たちはどう生きるか？」</li>
<li>事例別！Strict Concurrency対応方法</li>
<li>Migrating to Swift 6</li>
<li>Swift 6への移行</li>
<li>Adopting strict concurrency in Swift 6 apps</li>
<li>Updating an app to use strict concurrency | Apple Developer Documentation</li>
<li>MV State PatternでSwiftUIを受け入れる by Hiroma Tsuchiya</li>
<li>Monitoring model data changes in your app | Apple Developer Documentation</li>
<li>Data Flow Through SwiftUI - WWDC19 - Videos - Apple Developer</li>
<li>Swift 6のTyped throwsとSwiftにおけるエラーハンドリングの全体像を学ぶ by koher</li>
<li>Swift 6のTyped throwsとSwiftにおけるエラーハンドリングの全体像を学ぶ</li>
<li>SE-0413: Typed throws</li>
<li>Error Handling Rationale and Proposal</li>
<li>iOSDC Japan 2017 09&#x2F;16 Track B 11:20 &#x2F; 具体例とクイズで学ぶ、Swiftの4種類のエラーの使い分け &#x2F; Yuta Koshizawa</li>
<li>Appleの新しいプライバシー要件対応：ノーコード アプリプラットフォームの実践事例 by Nao-RandD &#x2F; ナオランド</li>
<li>Appleの新しいプライバシー要件対応： ノーコードアプリ プラットフォームの実践事例</li>
<li>ヤプリiOSエンジニアの新プライバシー要件対応① | 時系列で振り返り、関連用語を理解する</li>
<li>ヤプリiOSエンジニアの新プライバシー要件対応② | 実際に取り組んだ内容を大公開！</li>
<li>ヤプリiOSエンジニアの新プライバシー要件対応③ | ノーコードアプリプラットフォームの運用体制も大公開！</li>
<li>空間ビデオフォーマット、MV-HEVCが写し撮る世界 by なめき　ちはる</li>
<li>空間ビデオフォーマット、MV-HEVCが写し撮る世界 | Speaker Deck</li>
<li>これからの時代の新標準！Apple製テストフレームワーク「SwiftTesting」への移行とトラブルシューティング</li>
<li>これからの時代の新標準！SwiftTestingへの移行とトラブルシューティング</li>
<li>giginet&#x2F;swift-testing-revolutionary</li>
<li>医療アプリ開発の最前線 - 安全性と生産性の両立への挑戦 by Shogo Yoshida</li>
<li>医療アプリ開発の最前線 - 安全性と生産性の両立への挑戦 -</li>
</ul>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">初めてiOSDC Japan 2024にオフライン参加してきました！</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="Xcode" scheme="https://future-architect.github.io/tags/Xcode/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
    <category term="参加レポート" scheme="https://future-architect.github.io/tags/%E5%8F%82%E5%8A%A0%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
  </entry>
  <entry>
    <title>AirPods Proで頭の角度を検出し、リアルタイムにキャラクターを動かす</title>
    <link href="https://future-architect.github.io/articles/20240605a/"/>
    <id>https://future-architect.github.io/articles/20240605a/</id>
    <published>2024-06-04T15:00:00.000Z</published>
    <updated>2024-06-04T15:00:00.000Z</updated>
    <author><name>橋本竜我</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240605a/image.png" alt="" width="1200" height="390">

<h2 id="はじめに">はじめに</h2><p>HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>の橋本です。</p>
<p>先週末注文していたAirPods Pro第2世代が今日手元に届きました!</p>
<img src="/images/2024/20240605a/4f1c2557-bc97-063a-4e10-7f75b16d2449.jpeg" alt="" width="1200" height="923" loading="lazy">

<p>約4年間使っていたAirPods Pro第1世代の調子が悪くなってしまったため、買い換えました。</p>
<p>せっかく新しいAirPods Proが届いたので、なにかできることないかな〜と思いながら、AirPods Proの機能一覧を見ていました。</p>
<p>私はその中の1つ、空間オーディオ機能でヘッドトラッキングしていることに目をつけ、頭の角度の取得をしてみました。</p>
<h2 id="環境">環境</h2><ul>
<li>OS: macOS Sonoma 14.5</li>
<li>Xcode: 15.4 (15F31d)</li>
<li>Swift: 5.10</li>
<li>AirPods Pro(第2世代)</li>
</ul>
<p>※ 空間オーディオ機能搭載端末<br>AirPods(第３世代)、AirPods Pro(全世代)、AirPods Max<br>(参考URL: AirPodsユーザガイド 空間オーディオとヘッドトラッキングを操作する)</p>
<h2 id="今回作ったもの">今回作ったもの</h2><p>AirPodsProで取得した頭の角度でリアルタイムにキャラクター動かせるミニアプリ。</p>
<img src="/images/2024/20240605a/headTracking_2_(1).gif" alt="" width="154" height="334" loading="lazy">

<p>キャラクターが動いている部分は、次のように、私がAirPods Proをつけた状態で頭を傾けたり、回転したりしています。かなりよい感度で表示できているなと感じます。</p>
<img src="/images/2024/20240605a/画面収録_2024-05-30_23.08.00.gif" alt="" width="310" height="178" loading="lazy">

<p>このミニアプリで使用したフレームワークや各種APIを紹介していきたいと思います。</p>
<h3 id="CoreMotionフレームワークとは">CoreMotionフレームワークとは</h3><p>CoreMotionとは、各種Appleが提供するハードウェア（iOS, iPadOS, watchOS, visionOS device）のモーションデータを取得できるフレームワークです。</p>
<p>CoreMotionフレームワークのひとつのクラスとして、<code>CMHeadphoneMotionManager</code>があります。</p>
<h3 id="CMHeadphoneMotionManager">CMHeadphoneMotionManager</h3><p>このクラスを使うことで名前の通り、ヘッドフォンのモーション情報をアプリに提供することが可能になります。</p>
<p>使用するためには、<code>Info.plist</code>で<code>NSMotionUsageDescription</code>キーを追加し、モーションデータを使用する具体的な理由を記載する必要があります。</p>
<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>注意<br><code>NSMotionUsageDescription</code>キーが存在しない場合、対応するデバイスのモーションデータを更新しようとするとアプリがクラッシュしてしまいます。</p>
</div></div>

<p>今回使用する<code>CMHeadphoneMotionManager</code>が持つ主なメソッドについて、説明します。</p>
<h4 id="isDeviceMotionAvailable">isDeviceMotionAvailable</h4><p>接続している端末でCoreMotionが使えるか否かをBool値で返すメソッド。</p>
<h4 id="startDeviceMotionUpdates-to-withHandler">startDeviceMotionUpdates(to:withHandler)</h4><p>モーションデータの更新を開始し、データの更新を指定のキューに送信するメソッド。</p>
<h4 id="stopDeviceMotionUpdate">stopDeviceMotionUpdate()</h4><p>モーションデータの更新を停止するメソッド。</p>
<h2 id="実際にアプリを作ってみる">実際にアプリを作ってみる</h2><h3 id="事前準備">事前準備</h3><p>CoreMotionフレームワークが使えるように、Info.plistに<code>NSMotionUsageDescription</code>キーを追加、説明を記載します。</p>
<p>Info.plist</p>
<img src="/images/2024/20240605a/image_2.png" alt="image.png" width="1200" height="253" loading="lazy">

<p>project.pbxproj</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">INFOPLIST_KEY_NSMotionUsageDescription = <span class="string">&quot;To sync the movements of the character’s head on the screen with your head movements for an enhanced interactive experience.&quot;</span>;</span><br></pre></td></tr></table></figure>

<p>これで、次のようにモーションデータを取得する前に、許可を求められるポップアップが表示されます。</p>
<img src="/images/2024/20240605a/d972ec41-263c-c9de-0ebf-3dfce17b2334.jpeg" alt="" width="400" height="866" loading="lazy">

<h3 id="HeadTrackingを管理するクラスを用意する">HeadTrackingを管理するクラスを用意する</h3><p>HeadTrackingを監視するために、HeadTrackingManagerクラスを定義します。このクラスを使うことで、モーションデータを取得し、リアルタイムでSwiftUIビューに反映させることができます。</p>
<p>次に、コード全体を共有します。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> CoreMotion</span><br><span class="line"><span class="keyword">import</span> Foundation</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">HeadTrackingManager</span>: <span class="title class_ inherited__">ObservableObject</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">var</span> motionManager: <span class="type">CMHeadphoneMotionManager</span></span><br><span class="line">    <span class="meta">@Published</span> <span class="keyword">var</span> pitch: <span class="type">Double</span> <span class="operator">=</span> <span class="number">0.0</span></span><br><span class="line">    <span class="meta">@Published</span> <span class="keyword">var</span> roll: <span class="type">Double</span> <span class="operator">=</span> <span class="number">0.0</span></span><br><span class="line">    <span class="meta">@Published</span> <span class="keyword">var</span> yaw: <span class="type">Double</span> <span class="operator">=</span> <span class="number">0.0</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">init</span>() &#123;</span><br><span class="line">        motionManager <span class="operator">=</span> <span class="type">CMHeadphoneMotionManager</span>()</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// モーションデータのトラッキングを開始する</span></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">startTracking</span>() &#123;</span><br><span class="line">        <span class="comment">// 接続している端末でCoreMotionが使えるか否かを確認する</span></span><br><span class="line">        <span class="keyword">guard</span> motionManager.isDeviceMotionAvailable <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;Headphone motion data is not available.&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// モーションデータの更新を開始し、メインキューに送信する。</span></span><br><span class="line">        motionManager.startDeviceMotionUpdates(to: .main) &#123; [<span class="keyword">weak</span> <span class="keyword">self</span>] (motion, error) <span class="keyword">in</span></span><br><span class="line">            <span class="keyword">guard</span> <span class="keyword">let</span> <span class="keyword">self</span> <span class="operator">=</span> <span class="keyword">self</span>, <span class="keyword">let</span> motion <span class="operator">=</span> motion, error <span class="operator">==</span> <span class="literal">nil</span> <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">&quot;Error: <span class="subst">\(String(describing: error))</span>&quot;</span>)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 正常にモーションデータ取得後、頭の角度を取得する</span></span><br><span class="line">            <span class="type">Task</span> &#123;</span><br><span class="line">                <span class="keyword">await</span> <span class="keyword">self</span>.updateMotionData(motion)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// モーションデータの更新を停止する</span></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">stopTracking</span>() &#123;</span><br><span class="line">        motionManager.stopDeviceMotionUpdates()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// モーションデータから頭の角度（ピッチ、ロール、ヨー）を取得する</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">func</span> <span class="title function_">updateMotionData</span>(<span class="keyword">_</span> <span class="params">motion</span>: <span class="type">CMDeviceMotion</span>) <span class="keyword">async</span> &#123;</span><br><span class="line">        <span class="keyword">await</span> <span class="type">MainActor</span>.run &#123;</span><br><span class="line">            <span class="keyword">self</span>.pitch <span class="operator">=</span> motion.attitude.pitch</span><br><span class="line">            <span class="keyword">self</span>.roll <span class="operator">=</span> motion.attitude.roll</span><br><span class="line">            <span class="keyword">self</span>.yaw <span class="operator">=</span> motion.attitude.yaw</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="全体概要">全体概要</h3><p>このクラスは<code>ObservableObject</code>プロトコルに準拠させ、SwiftUIビューと連携して動作させるようにしています。</p>
<h3 id="各プロパティの説明">各プロパティの説明</h3><ul>
<li><code>motionManager: CMHeadphoneMotionManager</code><br>  MHeadphoneMotionManager のインスタンスで、ヘッドモーションデータの取得を管理する。</li>
<li><code>@Published var pitch: Double</code><br>  頭のピッチ（上下の回転角度）を表す。</li>
<li><code>@Published var roll: Double</code><br>  頭のロール（左右の傾き角度）を表す。</li>
<li><code>@Published var yaw: Double</code><br>  頭のヨー（水平面内の回転角度）を表す。<br>  これらのプロパティは、　<code>@Published</code>が付与されているので、変更があったときにはビューを自動的に更新する。</li>
</ul>
<p>次に、各メソッドを説明します。</p>
<h3 id="startTracking">startTracking()</h3><p>モーションデータの取得を開始するメソッド。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">func</span> <span class="title function_">startTracking</span>() &#123;</span><br><span class="line">        <span class="comment">// 接続している端末でCoreMotionが使えるか否かを確認する</span></span><br><span class="line">        <span class="keyword">guard</span> motionManager.isDeviceMotionAvailable <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;Headphone motion data is not available.&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// モーションデータの更新を開始し、メインキューに送信する。</span></span><br><span class="line">        motionManager.startDeviceMotionUpdates(to: .main) &#123; [<span class="keyword">weak</span> <span class="keyword">self</span>] (motion, error) <span class="keyword">in</span></span><br><span class="line">            <span class="keyword">guard</span> <span class="keyword">let</span> <span class="keyword">self</span> <span class="operator">=</span> <span class="keyword">self</span>, <span class="keyword">let</span> motion <span class="operator">=</span> motion, error <span class="operator">==</span> <span class="literal">nil</span> <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">&quot;Error: <span class="subst">\(String(describing: error))</span>&quot;</span>)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 正常にモーションデータ取得後、頭の角度を取得する</span></span><br><span class="line">            <span class="type">Task</span> &#123;</span><br><span class="line">                <span class="keyword">await</span> <span class="keyword">self</span>.updateMotionData(motion)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>

<p>モーションデータを提供できるかどうかを<code>isDeviceMotionAvailable</code>で確認します。その後、モーションデータの更新を開始し、データが取得されるたびに<code>Task</code>クロージャ内で、<code>updateMotionData(_:)</code>メソッドを呼び出します。</p>
<h3 id="stopTracking">stopTracking()</h3><p>モーションデータの取得を停止するメソッド。<code>stopDeviceMotionUpdates()</code> メソッドを呼び、モーションデータの取得を停止します。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">func</span> <span class="title function_">stopTracking</span>() &#123;</span><br><span class="line">    motionManager.stopDeviceMotionUpdates()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="updateMotionData-motion-CMDeviceMotion-async">updateMotionData(_ motion: CMDeviceMotion) async</h4><p>モーションデータを取得・更新するメソッド。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">func</span> <span class="title function_">updateMotionData</span>(<span class="keyword">_</span> <span class="params">motion</span>: <span class="type">CMDeviceMotion</span>) <span class="keyword">async</span> &#123;</span><br><span class="line">        <span class="keyword">await</span> <span class="type">MainActor</span>.run &#123;</span><br><span class="line">            <span class="keyword">self</span>.pitch <span class="operator">=</span> motion.attitude.pitch</span><br><span class="line">            <span class="keyword">self</span>.roll <span class="operator">=</span> motion.attitude.roll</span><br><span class="line">            <span class="keyword">self</span>.yaw <span class="operator">=</span> motion.attitude.yaw</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>

<p>UIを安全に更新するために、<code>MainActor.run</code>内でUIを更新することでUIスレッドの競合が発生しないようにしています。</p>
<p>取得した姿勢データ<code>attitude</code>を使用して、<code>pitch</code>、<code>roll</code>、<code>yaw</code>を更新します。</p>
<h3 id="Viewに反映させる">Viewに反映させる</h3><p>最後に、取得したデータを元にキャラクターを３次元空間でぐりぐり動かせるようにします。</p>
<p><code>Image()</code>をロール、ピッチ、ヨーで３次元回転させるように、<code>.rotation3DEffect()</code>を使用します。</p>
<p>ヨーに関しては、内側のカメラで自分を撮影するときと同様に、キャラクターが動くようにY軸周りの回転角を負の値に設定しています。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">Image</span>(<span class="string">&quot;monster&quot;</span>)</span><br><span class="line">    .resizable()</span><br><span class="line">    .aspectRatio(contentMode: .fit)</span><br><span class="line">    .frame(width: <span class="number">250</span>, height: <span class="number">250</span>)</span><br><span class="line">    .rotation3DEffect(<span class="type">Angle</span>(radians: headTrackingManager.roll), axis: (x: <span class="number">0</span>, y: <span class="number">0</span>, z: <span class="number">1</span>))</span><br><span class="line">    .rotation3DEffect(<span class="type">Angle</span>(radians: headTrackingManager.pitch), axis: (x: <span class="number">1</span>, y: <span class="number">0</span>, z: <span class="number">0</span>))</span><br><span class="line">    .rotation3DEffect(<span class="type">Angle</span>(radians: <span class="operator">-</span>(headTrackingManager.yaw)), axis: (x: <span class="number">0</span>, y: <span class="number">1</span>, z: <span class="number">0</span>))</span><br></pre></td></tr></table></figure>

<p>ボタンを押下するごとにHeadTrackingの計測状態と停止状態が入れ替わるようにします。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">Button</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> isTracking &#123;</span><br><span class="line">        headTrackingManager.stopTracking()</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        headTrackingManager.startTracking()</span><br><span class="line">    &#125;</span><br><span class="line">    isTracking.toggle()</span><br><span class="line">&#125; label: &#123;</span><br><span class="line">    <span class="type">Text</span>(isTracking <span class="operator">?</span> <span class="string">&quot;Stop Tracking&quot;</span> : <span class="string">&quot;Start Tracking&quot;</span>)</span><br><span class="line">        .padding()</span><br><span class="line">        .background(isTracking <span class="operator">?</span> <span class="type">Color</span>.red : <span class="type">Color</span>.green)</span><br><span class="line">        .foregroundColor(.white)</span><br><span class="line">        .cornerRadius(<span class="number">8</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>このボタンを押下することで、HeadTrackingが開始します。</p>
<p>全体のコードは次に記載しているので、手元で試してみてください。</p>
<h3 id="コード全体">コード全体</h3><details><summary>HeadTrackingApp.swift</summary>

<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> SwiftUI</span><br><span class="line"></span><br><span class="line"><span class="keyword">@main</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">HeadTrackingApp</span>: <span class="title class_ inherited__">App</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">Scene</span> &#123;</span><br><span class="line">        <span class="type">WindowGroup</span> &#123;</span><br><span class="line">            <span class="type">ContentView</span>()</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

</details>

<details><summary>HeadTrackingManager.swift</summary>

<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> CoreMotion</span><br><span class="line"><span class="keyword">import</span> Foundation</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">HeadTrackingManager</span>: <span class="title class_ inherited__">ObservableObject</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">var</span> motionManager: <span class="type">CMHeadphoneMotionManager</span></span><br><span class="line">    <span class="meta">@Published</span> <span class="keyword">var</span> pitch: <span class="type">Double</span> <span class="operator">=</span> <span class="number">0.0</span></span><br><span class="line">    <span class="meta">@Published</span> <span class="keyword">var</span> roll: <span class="type">Double</span> <span class="operator">=</span> <span class="number">0.0</span></span><br><span class="line">    <span class="meta">@Published</span> <span class="keyword">var</span> yaw: <span class="type">Double</span> <span class="operator">=</span> <span class="number">0.0</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">init</span>() &#123;</span><br><span class="line">        motionManager <span class="operator">=</span> <span class="type">CMHeadphoneMotionManager</span>()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">startTracking</span>() &#123;</span><br><span class="line">        <span class="keyword">guard</span> motionManager.isDeviceMotionAvailable <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;Headphone motion data is not available.&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        motionManager.startDeviceMotionUpdates(to: .main) &#123; [<span class="keyword">weak</span> <span class="keyword">self</span>] (motion, error) <span class="keyword">in</span></span><br><span class="line">            <span class="keyword">guard</span> <span class="keyword">let</span> <span class="keyword">self</span> <span class="operator">=</span> <span class="keyword">self</span>, <span class="keyword">let</span> motion <span class="operator">=</span> motion, error <span class="operator">==</span> <span class="literal">nil</span> <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">&quot;Error: <span class="subst">\(String(describing: error))</span>&quot;</span>)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="type">Task</span> &#123;</span><br><span class="line">                <span class="keyword">await</span> <span class="keyword">self</span>.updateMotionData(motion)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">stopTracking</span>() &#123;</span><br><span class="line">        motionManager.stopDeviceMotionUpdates()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">func</span> <span class="title function_">updateMotionData</span>(<span class="keyword">_</span> <span class="params">motion</span>: <span class="type">CMDeviceMotion</span>) <span class="keyword">async</span> &#123;</span><br><span class="line">        <span class="keyword">await</span> <span class="type">MainActor</span>.run &#123;</span><br><span class="line">            <span class="keyword">self</span>.pitch <span class="operator">=</span> motion.attitude.pitch</span><br><span class="line">            <span class="keyword">self</span>.roll <span class="operator">=</span> motion.attitude.roll</span><br><span class="line">            <span class="keyword">self</span>.yaw <span class="operator">=</span> motion.attitude.yaw</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

</details>

<details><summary>ContentView.swift</summary>

<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> SwiftUI</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">ContentView</span>: <span class="title class_ inherited__">View</span> &#123;</span><br><span class="line">    <span class="meta">@StateObject</span> <span class="keyword">private</span> <span class="keyword">var</span> headTrackingManager <span class="operator">=</span> <span class="type">HeadTrackingManager</span>()</span><br><span class="line">    <span class="meta">@State</span> <span class="keyword">private</span> <span class="keyword">var</span> isTracking <span class="operator">=</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">VStack</span>(alignment: .center) &#123;</span><br><span class="line">            <span class="type">Text</span>(<span class="string">&quot;Your head position&quot;</span>)</span><br><span class="line">                .font(.largeTitle)</span><br><span class="line"></span><br><span class="line">            <span class="type">Image</span>(<span class="string">&quot;monster&quot;</span>)</span><br><span class="line">                .resizable()</span><br><span class="line">                .aspectRatio(contentMode: .fit)</span><br><span class="line">                .frame(width: <span class="number">250</span>, height: <span class="number">250</span>)</span><br><span class="line">                .rotation3DEffect(<span class="type">Angle</span>(radians: headTrackingManager.roll), axis: (x: <span class="number">0</span>, y: <span class="number">0</span>, z: <span class="number">1</span>))</span><br><span class="line">                .rotation3DEffect(<span class="type">Angle</span>(radians: headTrackingManager.pitch), axis: (x: <span class="number">1</span>, y: <span class="number">0</span>, z: <span class="number">0</span>))</span><br><span class="line">                .rotation3DEffect(<span class="type">Angle</span>(radians: <span class="operator">-</span>(headTrackingManager.yaw)), axis: (x: <span class="number">0</span>, y: <span class="number">1</span>, z: <span class="number">0</span>))</span><br><span class="line"></span><br><span class="line">            <span class="type">HStack</span> &#123;</span><br><span class="line">                <span class="type">VStack</span>() &#123;</span><br><span class="line">                    <span class="type">Text</span>(<span class="string">&quot;Head Tracking Data&quot;</span>)</span><br><span class="line">                        .font(.title)</span><br><span class="line">                    <span class="type">Text</span>(<span class="string">&quot;Pitch: <span class="subst">\(headTrackingManager.pitch)</span>&quot;</span>)</span><br><span class="line">                    <span class="type">Text</span>(<span class="string">&quot;Roll: <span class="subst">\(headTrackingManager.roll)</span>&quot;</span>)</span><br><span class="line">                    <span class="type">Text</span>(<span class="string">&quot;Yaw: <span class="subst">\(headTrackingManager.yaw)</span>&quot;</span>)</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">HStack</span> &#123;</span><br><span class="line">            <span class="type">Button</span> &#123;</span><br><span class="line">                <span class="keyword">if</span> isTracking &#123;</span><br><span class="line">                    headTrackingManager.stopTracking()</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    headTrackingManager.startTracking()</span><br><span class="line">                &#125;</span><br><span class="line">                isTracking.toggle()</span><br><span class="line">            &#125; label: &#123;</span><br><span class="line">                <span class="type">Text</span>(isTracking <span class="operator">?</span> <span class="string">&quot;Stop Tracking&quot;</span> : <span class="string">&quot;Start Tracking&quot;</span>)</span><br><span class="line">                    .padding()</span><br><span class="line">                    .background(isTracking <span class="operator">?</span> <span class="type">Color</span>.red : <span class="type">Color</span>.green)</span><br><span class="line">                    .foregroundColor(.white)</span><br><span class="line">                    .cornerRadius(<span class="number">8</span>)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

</details>

<h2 id="さいごに">さいごに</h2><p><code>CMHeadphoneMotionManager</code>を使うことでとても簡単にHeadTrackingを行うことができました！</p>
<p>今後は、今回取得した頭の角度データを時系列データとして扱い、<code>HealthKit</code>フレームワークの<code>HKHealthStore</code>に保存したりして健康促進に活用できないか等、色々検討したいと思います。</p>
<h2 id="参考">参考</h2><ul>
<li>AirPods Pro（第2世代）<br>https://www.apple.com/jp/airpods-pro/</li>
<li>空間オーディオとヘッドトラッキングを操作する<br>https://support.apple.com/ja-jp/guide/airpods/dev00eb7e0a3/web</li>
<li>AirPods Proの加速度センサーの値を取得する【iOS14】<br>https://qiita.com/tukutuku_tukuyo/items/ea949ee2dbb499d6e7ca</li>
<li>Core Motion | Apple Developer Documentation<br>https://developer.apple.com/documentation/coremotion</li>
<li>CMHeadphoneMotionManager | Apple Developer Documentation<br>https://developer.apple.com/documentation/coremotion/cmheadphonemotionmanager</li>
<li>isDeviceMotionAvailable | Apple Developer Documentation<br>https://developer.apple.com/documentation/coremotion/cmmotionmanager/1616094-isdevicemotionavailable</li>
<li>stopDeviceMotionUpdates() | Apple Developer Documentation<br>https://developer.apple.com/documentation/coremotion/cmmotionmanager/1616115-stopdevicemotionupdates</li>
<li>startDeviceMotionUpdates(to:withHandler) | Apple Developer Documentation<br>https://developer.apple.com/documentation/coremotion/cmmotionmanager/1616048-startdevicemotionupdates</li>
<li>かわいいフリー素材集『いらすとや』<br>https://www.irasutoya.com/</li>
</ul>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは次の記事をご覧ください。”新規事業の立ち上げ　フューチャーの知られざる医療・ヘルスケアへの挑戦”</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">AirPods Proの空間オーディオ機能にあるヘッドトラッキングを用いることで、頭の角度の取得をしてみました</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Apple" scheme="https://future-architect.github.io/tags/Apple/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="SwiftUI" scheme="https://future-architect.github.io/tags/SwiftUI/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
  </entry>
  <entry>
    <title>Develop in SwiftでSwiftDataの基本を学ぶ ～Models and persistence編～</title>
    <link href="https://future-architect.github.io/articles/20240522a/"/>
    <id>https://future-architect.github.io/articles/20240522a/</id>
    <published>2024-05-21T15:00:00.000Z</published>
    <updated>2024-05-21T15:00:00.000Z</updated>
    <author><name>橋本竜我</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240522a/image.png" alt="" width="1103" height="542">

<h2 id="はじめに">はじめに</h2><p>HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>の橋本です。</p>
<p>新しく登場した公式チュートリアルDevelop in SwiftのModels and persistence、Data editing and navigation、Relationships and queriesという3つのセクションでSwiftDataを学べるとのことで実際にやってみました。</p>
<p>今回は、1つ目の <strong>Models and persistence編</strong> です。Models and persistenceセクションで学んだこと、Wrap-upのExtend your appの追加課題をやってみたので、これらについてまとめています。</p>
<h2 id="本記事でわかること">本記事でわかること</h2><ul>
<li><code>SwiftData</code>の基本的な使い方<ul>
<li><code>SwiftData</code>の導入</li>
<li><code>@Model</code>, <code>@Query</code>の使い方</li>
<li><code>SwiftUI</code>の<code>View</code>との連携</li>
</ul>
</li>
<li>Wrap-upのExtend your appの追加課題の解答例を知ることができる</li>
</ul>
<h2 id="環境">環境</h2><ul>
<li>OS: macOS Sonoma 14.4.1</li>
<li>Xcode: 15.3 (15E204a)</li>
<li>Swift: 5.10</li>
</ul>
<h2 id="SwiftDataとは">SwiftDataとは</h2><img src="/images/2024/20240522a/image_2.png" alt="" width="1200" height="419" loading="lazy">

<p><code>SwiftData</code>とは、データモデリングとデータの永続化のフレームワークです。<br>これまで主に使われていた<code>CoreData</code>の後継として期待されています。<br>SwiftDataの主な特徴としては、</p>
<ul>
<li><code>SwiftUI</code>との連携<ul>
<li><code>SwiftUI</code>と深く統合されており、ユーザーインターフェースのためのデータバインディングが非常にスムーズ</li>
</ul>
</li>
<li>宣言的データモデリング:<ul>
<li>属性、関係性、オブジェクトのバリデーションなどを宣言的に定義できること</li>
</ul>
</li>
<li>データ永続化:<ul>
<li>オブジェクトが自動的に保存され、データ永続化が容易であること</li>
</ul>
</li>
<li>パフォーマンス最適化:<ul>
<li>Appleのエコシステムに合わせて最適化されており、効率的なクエリ実行が可能</li>
</ul>
</li>
</ul>
<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>注意<br>SwiftDataはiOS17.0+, iPadOS17.0+で使用可能であること。</p>
</div></div>

<h2 id="Save-data">Save data</h2><p>Models and persistenceセクションのSave dataを進めていきます。</p>
<h3 id="Section-1-3-UI等のSwiftDataに直接関係のない事前準備">Section 1~3(UI等のSwiftDataに直接関係のない事前準備)</h3><p>以下は、Section1~3まで対応後のコードです。この時点では<code>SwiftData</code>を使用していないため、誕生日を登録しても、アプリキルすると登録したデータは削除されてしまいます。</p>
<details><summary>BirthdayApp.swift</summary>

<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> SwiftUI</span><br><span class="line"></span><br><span class="line"><span class="keyword">@main</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">BirthdaysApp</span>: <span class="title class_ inherited__">App</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">Scene</span> &#123;</span><br><span class="line">        <span class="type">WindowGroup</span> &#123;</span><br><span class="line">            <span class="type">ContentView</span>()</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

</details>

<details><summary>Friend.swift</summary>

<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> Foundation</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Friend</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> name: <span class="type">String</span></span><br><span class="line">    <span class="keyword">let</span> birthday: <span class="type">Date</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

</details>

<details><summary>ContentView.swift</summary>

<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> SwiftUI</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">ContentView</span>: <span class="title class_ inherited__">View</span> &#123;</span><br><span class="line">    <span class="meta">@State</span> <span class="keyword">private</span> <span class="keyword">var</span> friends: [<span class="type">Friend</span>] <span class="operator">=</span> [</span><br><span class="line">        <span class="type">Friend</span>(name: <span class="string">&quot;Elton&quot;</span>, birthday: .now),</span><br><span class="line">        <span class="type">Friend</span>(name: <span class="string">&quot;Jenny Court&quot;</span>, birthday: <span class="type">Date</span>(timeIntervalSince1970: <span class="number">0</span>))</span><br><span class="line">    ]</span><br><span class="line"></span><br><span class="line">    <span class="meta">@State</span> <span class="keyword">private</span> <span class="keyword">var</span> newName <span class="operator">=</span> <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="meta">@State</span> <span class="keyword">private</span> <span class="keyword">var</span> newDate <span class="operator">=</span> <span class="type">Date</span>.now</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">NavigationStack</span> &#123;</span><br><span class="line">            <span class="type">List</span>(friends, id: \.name) &#123; friend <span class="keyword">in</span></span><br><span class="line">                <span class="type">HStack</span> &#123;</span><br><span class="line">                    <span class="type">Text</span>(friend.name)</span><br><span class="line">                    <span class="type">Spacer</span>()</span><br><span class="line">                    <span class="type">Text</span>(friend.birthday, format: .dateTime.month(.wide).day().year())</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            .navigationTitle(<span class="string">&quot;Birthdays&quot;</span>)</span><br><span class="line">            .safeAreaInset(edge: .bottom) &#123;</span><br><span class="line">                <span class="type">VStack</span>(alignment: .center, spacing: <span class="number">20</span>) &#123;</span><br><span class="line">                    <span class="type">Text</span>(<span class="string">&quot;New Birthday&quot;</span>)</span><br><span class="line">                        .font(.headline)</span><br><span class="line">                    <span class="type">DatePicker</span>(selection: <span class="variable">$newDate</span>, in: <span class="type">Date</span>.distantPast<span class="operator">...</span><span class="type">Date</span>.now, displayedComponents: .date) &#123;</span><br><span class="line">                        <span class="type">TextField</span>(<span class="string">&quot;Name&quot;</span>, text: <span class="variable">$newName</span>)</span><br><span class="line">                            .textFieldStyle(.roundedBorder)</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="type">Button</span>(<span class="string">&quot;Save&quot;</span>) &#123;</span><br><span class="line">                        <span class="keyword">let</span> newFriend <span class="operator">=</span> <span class="type">Friend</span>(name: newName, birthday: newDate)</span><br><span class="line">                        friends.append(newFriend)</span><br><span class="line">                        newName <span class="operator">=</span> <span class="string">&quot;&quot;</span></span><br><span class="line">                        newDate <span class="operator">=</span> .now</span><br><span class="line">                    &#125;</span><br><span class="line">                    .bold()</span><br><span class="line">                &#125;</span><br><span class="line">                .padding()</span><br><span class="line">                .background(.bar)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

</details>

<h3 id="Section４-Convert-your-structure-to-a-SwiftData-model">Section４: Convert your structure to a SwiftData model</h3><p>先程作成した<code>Friend</code>構造体を<code>SwiftData</code>モデルに変換します。</p>
<p>以下、4点を対応します。</p>
<ul>
<li><code>SwiftData</code>フレームワークをインポートします</li>
<li>モデルに対して、<code>@Model</code>マクロのアノテーションを付与します</li>
<li><code>struct</code>から<code>class</code>に書き換えます</li>
<li><code>class</code>は<code>struct</code>と異なり、自動でイニシャライザが生成されないため、イニシャライザを用意します</li>
</ul>
<p><strong>Friend.swift</strong></p>
<figure class="highlight diff"><table><tr><td class="code"><pre><span class="line">import Foundation</span><br><span class="line"><span class="addition">+ import SwiftData</span></span><br><span class="line"></span><br><span class="line"><span class="addition">+ @Model</span></span><br><span class="line"><span class="deletion">- struct Friend &#123;</span></span><br><span class="line"><span class="addition">+ class Frined &#123;</span></span><br><span class="line">    let name: String</span><br><span class="line">    let birthday: Date</span><br><span class="line"></span><br><span class="line"><span class="addition">+    init(name: String, birthday: Date) &#123;</span></span><br><span class="line"><span class="addition">+    self.name = name</span></span><br><span class="line"><span class="addition">+    self.birthday = birthday</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="Section5-Connect-SwiftData-and-SwiftUI">Section5: Connect SwiftData and SwiftUI</h3><p><code>SwiftData</code>と<code>SwiftUI</code>の<code>View</code>を連携させます。<br><code>SwiftUI</code>におけるエントリーポイントである<code>~App.swift</code>に次のコードを追加することで、<code>SwiftData</code>によって永続化させたデータの保存場所と<code>View</code>を連携させることができます。</p>
<p><strong>BirthdaysApp.swift</strong></p>
<figure class="highlight diff"><table><tr><td class="code"><pre><span class="line">import SwiftUI</span><br><span class="line"><span class="addition">+ import SwiftData</span></span><br><span class="line"></span><br><span class="line">@main</span><br><span class="line">struct BirthdaysApp: App &#123;</span><br><span class="line">    var body: some Scene &#123;</span><br><span class="line">        WindowGroup &#123;</span><br><span class="line">            ContentView()</span><br><span class="line"><span class="addition">+                .modelContainer(for: Friend.self)</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong>ContentView.swift</strong></p>
<ul>
<li><code>friends</code>配列を<code>@State</code>から<code>@Query</code>に変更することで、<code>@Query</code>マクロによって、モデルに変更があったときに<code>SwiftUIView</code>に自動的に変更を伝えることができます。</li>
<li><code>modelContext</code>を環境変数として宣言します。<ul>
<li><code>SwiftData</code>において、<code>ModelContext</code>によってビューとモデルコンテナ間を接続し、データの取得、挿入、削除が可能になります。</li>
</ul>
</li>
</ul>
<figure class="highlight diff"><table><tr><td class="code"><pre><span class="line">import SwiftUI</span><br><span class="line"><span class="addition">+ import SwiftData</span></span><br><span class="line"></span><br><span class="line">struct ContentView: View &#123;</span><br><span class="line"><span class="addition">+   @Query private var friends: [Friend]</span></span><br><span class="line"><span class="deletion">-   @State private var friends: [Friend] = [</span></span><br><span class="line"><span class="deletion">-       Friend(name: &quot;Elton&quot;, birthday: .now),</span></span><br><span class="line"><span class="deletion">-       Friend(name: &quot;Jenny Court&quot;, birthday: Date(timeIntervalSince1970: 0))</span></span><br><span class="line"><span class="deletion">-   ]</span></span><br><span class="line"><span class="addition">+   @Environment(\.modelContext) private var context</span></span><br><span class="line"></span><br><span class="line">    @State private var newName = &quot;&quot;</span><br><span class="line">    @State private var newDate = Date.now</span><br><span class="line"></span><br><span class="line">    var body: some View &#123;</span><br><span class="line">        NavigationStack &#123;</span><br><span class="line">            List(friends, id: \.name) &#123; friend in</span><br><span class="line">                HStack &#123;</span><br><span class="line">                    Text(friend.name)</span><br><span class="line">                    Spacer()</span><br><span class="line">                    Text(friend.birthday, format: .dateTime.month(.wide).day().year())</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            .navigationTitle(&quot;Birthdays&quot;)</span><br><span class="line">            .safeAreaInset(edge: .bottom) &#123;</span><br><span class="line">                VStack(alignment: .center, spacing: 20) &#123;</span><br><span class="line">                    Text(&quot;New Birthday&quot;)</span><br><span class="line">                        .font(.headline)</span><br><span class="line">                    DatePicker(selection: $newDate, in: Date.distantPast...Date.now, displayedComponents: .date) &#123;</span><br><span class="line">                        TextField(&quot;Name&quot;, text: $newName)</span><br><span class="line">                            .textFieldStyle(.roundedBorder)</span><br><span class="line">                    &#125;</span><br><span class="line">                    Button(&quot;Save&quot;) &#123;</span><br><span class="line">                        let newFriend = Friend(name: newName, birthday: newDate)</span><br><span class="line"><span class="addition">+                       context.insert(newFriend)</span></span><br><span class="line"><span class="deletion">-                       friends.append(newFriend)</span></span><br><span class="line">                        newName = &quot;&quot;</span><br><span class="line">                        newDate = .now</span><br><span class="line">                    &#125;</span><br><span class="line">                    .bold()</span><br><span class="line">                &#125;</span><br><span class="line">                .padding()</span><br><span class="line">                .background(.bar)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="Section6-Use-model-data-to-fill-out-the-UI">Section6: Use model data to fill out the UI</h3><p>モデルデータを使ってUIをいい感じに整えていきます。<br>主に以下2点を実現させます。</p>
<ul>
<li>登録されているデータを日付順でソートする。</li>
<li>誕生日当日の人は、ケーキのマークがつくようにする。</li>
</ul>
<p><strong>BirthDayApp.swift</strong><br>Section5から追加の修正なし。</p>
<p><strong>Friend.swift</strong></p>
<p>今日が誕生日かどうかを表すために、コンピューテッドプロパティとして、<code>Bool</code>型の<code>isBirthdayToday</code>を用意します。</p>
<figure class="highlight diff"><table><tr><td class="code"><pre><span class="line">import Foundation</span><br><span class="line">import SwiftData</span><br><span class="line"></span><br><span class="line">@Model</span><br><span class="line">class Frined &#123;</span><br><span class="line">    let name: String</span><br><span class="line">    let birthday: Date</span><br><span class="line"></span><br><span class="line">    init(name: String, birthday: Date) &#123;</span><br><span class="line">    self.name = name</span><br><span class="line">    self.birthday = birthday</span><br><span class="line"></span><br><span class="line"><span class="addition">+   var isBirthdayToday: Bool &#123;</span></span><br><span class="line"><span class="addition">+       Calendar.current.isDateInToday(birthday)</span></span><br><span class="line"><span class="addition">+   &#125;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong>ContentView.swift</strong></p>
<ul>
<li>追加された人の誕生日を降順で並ぶように、<code>@Query</code>を<code>@Query(sort: \Friend.birthday)</code>に修正します。<code>@Query</code> だけの場合は、いつも同じ並び順にはならないことに注意してください。</li>
<li><code>SwiftData</code>は各モデルインスタンスに独自のIDを提供します。<code>List</code>では、<code>@Model</code> が提供する識別子を使うため、明示的な<code>ID</code>を削除します。つまり、<code>List()</code>の引数である<code>KeyPath</code>を削除します。</li>
</ul>
<figure class="highlight diff"><table><tr><td class="code"><pre><span class="line">import SwiftUI</span><br><span class="line">import SwiftData</span><br><span class="line"></span><br><span class="line">struct ContentView: View &#123;</span><br><span class="line"><span class="addition">+   @Query(sort: \Friend.birthday) private var friends: [Friend]</span></span><br><span class="line"><span class="deletion">-   @Query private var friends: [Friend]</span></span><br><span class="line">    @Environment(\.modelContext) private var context</span><br><span class="line"></span><br><span class="line">    @State private var newName = &quot;&quot;</span><br><span class="line">    @State private var newDate = Date.now</span><br><span class="line"></span><br><span class="line">    var body: some View &#123;</span><br><span class="line">        NavigationStack &#123;</span><br><span class="line"><span class="addition">+           List(friends) &#123; friend in</span></span><br><span class="line"><span class="deletion">-           List(friends, id: \.name) &#123; friend in</span></span><br><span class="line">                HStack &#123;</span><br><span class="line">                    Text(friend.name)</span><br><span class="line">                    Spacer()</span><br><span class="line">                    Text(friend.birthday, format: .dateTime.month(.wide).day().year())</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            .navigationTitle(&quot;Birthdays&quot;)</span><br><span class="line">            .safeAreaInset(edge: .bottom) &#123;</span><br><span class="line">                VStack(alignment: .center, spacing: 20) &#123;</span><br><span class="line">                    Text(&quot;New Birthday&quot;)</span><br><span class="line">                        .font(.headline)</span><br><span class="line">                    DatePicker(selection: $newDate, in: Date.distantPast...Date.now, displayedComponents: .date) &#123;</span><br><span class="line">                        TextField(&quot;Name&quot;, text: $newName)</span><br><span class="line">                            .textFieldStyle(.roundedBorder)</span><br><span class="line">                    &#125;</span><br><span class="line">                    Button(&quot;Save&quot;) &#123;</span><br><span class="line">                        let newFriend = Friend(name: newName, birthday: newDate)</span><br><span class="line">                        context.insert(newFriend)</span><br><span class="line">                        newName = &quot;&quot;</span><br><span class="line">                        newDate = .now</span><br><span class="line">                    &#125;</span><br><span class="line">                    .bold()</span><br><span class="line">                &#125;</span><br><span class="line">                .padding()</span><br><span class="line">                .background(.bar)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="Wrap-up-Models-and-persistence">Wrap-up: Models and persistence</h3><p>次のページの<code>Extend your app</code>の次の２つのお題に取り組みたいと思います。</p>
<p>https://developer.apple.com/tutorials/develop-in-swift/models-and-persistence-conclusion</p>
<h4 id="Extend-your-app（ソートする基準の変更、降順or昇順）">Extend your app（ソートする基準の変更、降順or昇順）</h4><h5 id="Sort-the-birthday-list-by-name-instead-of-birthday">Sort the birthday list by name instead of birthday.</h5><p>これはとても簡単に修正できます。<br><code>@Query(sort: \Friend.birthday)</code>を<code>@Query(sort: \Friend.name)</code>に変えるだけです。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Query</span>(sort: \<span class="type">Friend</span>.name) <span class="keyword">private</span> <span class="keyword">var</span> friends: [<span class="type">Friend</span>]</span><br></pre></td></tr></table></figure>

<p>これで、誕生日順ではなく、名前の降順にできました。これを昇順にする場合は、引数<code>order: .reverse</code>を与えるだけで実現できます。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Query</span>(sort: \<span class="type">Friend</span>.name, order: .reverse) <span class="keyword">private</span> <span class="keyword">var</span> friends: [<span class="type">Friend</span>]</span><br></pre></td></tr></table></figure>

<h5 id="Add-a-notes-property-to-Friend-to-plan-how-you’ll-celebrate-a-friend’s-birthday">Add a notes property to Friend to plan how you’ll celebrate a friend’s birthday.</h5><p>これもおまけ的な内容ですが、簡単に実装してみます。</p>
<details><summary>Friend.swift</summary>

<figure class="highlight diff"><table><tr><td class="code"><pre><span class="line">import Foundation</span><br><span class="line">import SwiftData</span><br><span class="line"></span><br><span class="line">@Model</span><br><span class="line">class Frined &#123;</span><br><span class="line">    let name: String</span><br><span class="line">    let birthday: Date</span><br><span class="line"><span class="addition">+   let notes: String</span></span><br><span class="line"></span><br><span class="line"><span class="addition">+   init(name: String, birthday: Date, notes: String) &#123;</span></span><br><span class="line"><span class="deletion">-   init(name: String, birthday: Date) &#123;</span></span><br><span class="line">    self.name = name</span><br><span class="line">    self.birthday = birthday</span><br><span class="line"><span class="addition">+   self.notes = notes</span></span><br><span class="line"></span><br><span class="line">    var isBirthdayToday: Bool &#123;</span><br><span class="line">        Calendar.current.isDateInToday(birthday)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

</details>

<details><summary>ContentView.swift</summary>

<figure class="highlight diff"><table><tr><td class="code"><pre><span class="line">import SwiftUI</span><br><span class="line">import SwiftData</span><br><span class="line"></span><br><span class="line">struct ContentView: View &#123;</span><br><span class="line">    @Query(sort: \Friend.birthday) private var friends: [Friend]</span><br><span class="line">    @Environment(\.modelContext) private var context</span><br><span class="line"></span><br><span class="line">    @State private var newName = &quot;&quot;</span><br><span class="line">    @State private var newDate = Date.now</span><br><span class="line"><span class="addition">+   @State private var newNotes = &quot;&quot;</span></span><br><span class="line"></span><br><span class="line">    var body: some View &#123;</span><br><span class="line">        NavigationStack &#123;</span><br><span class="line">            List(friends) &#123; friend in</span><br><span class="line">                HStack &#123;</span><br><span class="line">                    if friend.isBirthdayToday &#123;</span><br><span class="line">                        Image(systemName: &quot;birthday.cake&quot;)</span><br><span class="line">                    &#125;</span><br><span class="line">                    VStack &#123;</span><br><span class="line">                        Text(friend.name)</span><br><span class="line">                            .bold(friend.isBirthdayToday)</span><br><span class="line"><span class="addition">+                       Text(friend.notes)</span></span><br><span class="line"><span class="addition">+                           .font(.caption2)</span></span><br><span class="line">                    &#125;</span><br><span class="line">                    Spacer()</span><br><span class="line">                    Text(friend.birthday, format: .dateTime.month(.wide).day().year())</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            .navigationTitle(&quot;Birthdays&quot;)</span><br><span class="line">            .safeAreaInset(edge: .bottom) &#123;</span><br><span class="line">                VStack(alignment: .center, spacing: 20) &#123;</span><br><span class="line">                    Text(&quot;New Birthday&quot;)</span><br><span class="line">                        .font(.headline)</span><br><span class="line">                    DatePicker(selection: $newDate, in: Date.distantPast...Date.now, displayedComponents: .date) &#123;</span><br><span class="line">                        TextField(&quot;Name&quot;, text: $newName)</span><br><span class="line">                            .textFieldStyle(.roundedBorder)</span><br><span class="line">                    &#125;</span><br><span class="line"><span class="addition">+                   TextField(&quot;Notes&quot;, text: $newNotes)</span></span><br><span class="line"><span class="addition">+                       .textFieldStyle(.roundedBorder)</span></span><br><span class="line">                    Button(&quot;Save&quot;) &#123;</span><br><span class="line">                        let newFriend = Friend(name: newName, birthday: newDate, notes: newNotes)</span><br><span class="line">                        context.insert(newFriend)</span><br><span class="line">                        newName = &quot;&quot;</span><br><span class="line">                        newDate = .now</span><br><span class="line"><span class="addition">+                       newNotes = &quot;&quot;</span></span><br><span class="line">                    &#125;</span><br><span class="line">                    .bold()</span><br><span class="line">                &#125;</span><br><span class="line">                .padding()</span><br><span class="line">                .background(.bar)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

</details>

<h5 id="完成したサンプルアプリ">完成したサンプルアプリ</h5><p>追加した友達の誕生日のデータがアプリをキルしても、再度立ち上げると残っていることが確認できました！<br><code>SwiftData</code>を使って、データを永続化させることに成功！！</p>
<img src="/images/2024/20240522a/Simulator_Screen_Recording_-_iPhone_15_-_2024-05-15_at_20.26.56.gif" alt="" width="218" height="474" loading="lazy">

<h2 id="おわりに">おわりに</h2><p>公式チュートリアルDevelop in Swiftを使って<code>SwiftData</code>の基本的な使い方を学びました。気になった方は、実際にチュートリアルを1つずつ実際にコードを書きながら、進めることをおすすめします。</p>
<p>次回は、公式チュートリアルDevelop in SwiftのData editing and navigation編を公開したいと思います。</p>
<h2 id="参考">参考</h2><ul>
<li>SwiftData | Apple Developer Document https://developer.apple.com/documentation/swiftdata</li>
<li>Meet SwiftData https://developer.apple.com/videos/play/wwdc2023/10187/</li>
<li>Develop in Swift https://developer.apple.com/tutorials/develop-in-swift</li>
<li>Meet SwiftData - WWDC2023 https://developer.apple.com/videos/play/wwdc2023/10187/</li>
<li>SwiftData入門 https://zenn.dev/yumemi_inc/articles/2a929c839b2000</li>
</ul>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは次の記事をご覧ください。”新規事業の立ち上げ　フューチャーの知られざる医療・ヘルスケアへの挑戦”</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">公式チュートリアルDevelop in SwiftのModels and persistence、Data editing and navigation、Relationships and queriesという3つのセクションでSwiftDataを学べるとことで実際にやってみました。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="SwiftUI" scheme="https://future-architect.github.io/tags/SwiftUI/"/>
  </entry>
  <entry>
    <title>DJI Mobile SDKのサンプルコードでシミュレータを動かしてみる</title>
    <link href="https://future-architect.github.io/articles/20240521a/"/>
    <id>https://future-architect.github.io/articles/20240521a/</id>
    <published>2024-05-20T15:00:00.000Z</published>
    <updated>2024-05-20T15:00:00.000Z</updated>
    <author><name>大前七奈</name></author>
    <content type="html"><![CDATA[<h2 id="やりたいこと">やりたいこと</h2><p>ドローンを利用して自動操縦（Autopilot）のロジックを組みたいですが、自宅でドローンを飛ばしたら家が壊されてしまう恐れがあるので、シミュレータを使いたいなと思いました。</p>
<p>調査した結果、DJI Assistant 2というアプリがあり、開発PCでデバッグしながら動きを見ることができるそうです。</p>
<p>早速、作業に入りましょー！</p>
<h2 id="必要なもの">必要なもの</h2><ul>
<li>DJI Phantom 4</li>
<li>リモートコントローラー(以下、RC)</li>
<li>USBケーブル数本</li>
</ul>
<img fetchpriority="high" src="/images/2024/20240521a/image.png" alt="" width="1200" height="904">

<h2 id="バージョン">バージョン</h2><ul>
<li>開発PC<ul>
<li>Android Studio Hedgedog</li>
<li>Android SDK API Level 31</li>
<li>Gradle 7.5</li>
<li>Gradle JDK Correcto-11</li>
</ul>
</li>
<li>シミュレータ用PC<ul>
<li>DJI Assistant 2 for Phantom</li>
</ul>
</li>
<li>Bridge App用携帯<ul>
<li>DJI Bridge App</li>
</ul>
</li>
</ul>
<h2 id="システム構成">システム構成</h2><img src="/images/2024/20240521a/image_2.png" alt="image.png" width="1200" height="426" loading="lazy">

<h2 id="手順">手順</h2><h3 id="１（開発PC）DJI-Mobile-SDK-V4をクローン">１（開発PC）DJI Mobile SDK V4をクローン</h3><p>https://github.com/dji-sdk/Mobile-SDK-Android/tree/master</p>
<h3 id="２（開発PC）DJI-DeveloperでAPIキーを発行">２（開発PC）DJI DeveloperでAPIキーを発行</h3><ul>
<li>DJI Developerでログイン</li>
<li>「CREATE APP」でアプリケーションを登録する</li>
</ul>
<p>※Package Nameはサンプルコード内のパッケージ名（各パッケージが存在するパス：Sample Code&#x2F;app&#x2F;src&#x2F;main&#x2F;java）。その他の項目は適当に記載</p>
<img src="/images/2024/20240521a/image_3.png" alt="image.png" width="1192" height="1130" loading="lazy">

<h3 id="３（開発PC）サンプリコード内にAPIキーを入力">３（開発PC）サンプリコード内にAPIキーを入力</h3><p>AndroidManifest.xml（パス：Sample Code&#x2F;app&#x2F;src&#x2F;main&#x2F;main）</p>
<img src="/images/2024/20240521a/image_4.png" alt="image.png" width="1200" height="405" loading="lazy">

<h3 id="４（開発PC）Sample-Codeフォルダーを開いてビルドする">４（開発PC）Sample Codeフォルダーを開いてビルドする</h3><ul>
<li>gradle-wrapper.properties(パス：Sample Code&#x2F;gradle&#x2F;wrapper)で一度「Sync」OR「build」<br>※まず最初からSyncするといいと思うが、大抵の場合ビルドエラーが起こる。こんな感じ</li>
</ul>
<img src="/images/2024/20240521a/image_5.png" alt="image.png" width="1200" height="569" loading="lazy">

<ul>
<li>「Change Gradle version in Gradle wrapper to 7.5….」を押すと、自動的にgradle-wrapper.propertiesの中身を書き換えてくれて、ビルドする</li>
</ul>
<img src="/images/2024/20240521a/image_6.png" alt="image.png" width="1200" height="454" loading="lazy">

<h3 id="５（開発PC）エミュレータを起動">５（開発PC）エミュレータを起動</h3><img src="/images/2024/20240521a/image_7.png" alt="image.png" width="794" height="1594" loading="lazy">

<ul>
<li>起動後に「Register App」を押すと、DJI SDK利用が可能になる</li>
</ul>
<h3 id="６（BridgeApp用携帯）携帯にBridgeAppをインストール">６（BridgeApp用携帯）携帯にBridgeAppをインストール</h3><ul>
<li>iOSはApp Storeからダウンロードする</li>
<li>Andriodはdji-sdk&#x2F;Android-Bridge-Appからapk経由でインストール</li>
</ul>
<h3 id="７（BridgeApp用携帯）Bridge-AppでRCに接続">７（BridgeApp用携帯）Bridge AppでRCに接続</h3><ul>
<li>携帯は開発者モードをONにし、USB DebuggingをONにする</li>
<li>RCの電源をON、Phantom 4の電源をONにする</li>
<li>Bridge Appを立ち上げて、USBでRCに繋ぐ</li>
</ul>
<img src="/images/2024/20240521a/image_8.png" alt="image.png" width="880" height="1868" loading="lazy">

<ul>
<li>画面のIPアドレスをメモる(こちらの端末では192.168.1.35)</li>
</ul>
<h3 id="８（開発PC）IPアドレスをエミュレータ画面に入力">８（開発PC）IPアドレスをエミュレータ画面に入力</h3><ul>
<li>「WSBridge IP」欄にIPアドレスを入力</li>
</ul>
<p>(開発PC画面)</p>
<img src="/images/2024/20240521a/image_9.png" alt="image.png" width="772" height="1570" loading="lazy">

<p>(BridgeApp用携帯画面)</p>
<img src="/images/2024/20240521a/image_10.png" alt="image.png" width="888" height="1868" loading="lazy">

<h3 id="９（シミュレータ用PC）DJI-Assistant-2でPhantom-4に接続">９（シミュレータ用PC）DJI Assistant 2でPhantom 4に接続</h3><ul>
<li><p>DJI Assistant 2 for Phantomを立ち上げて、USBでPhantom 4に繋ぐ</p>
</li>
<li><p>このアプリを使うには、以下の制限と設定が必要です（少し面倒ですね…）</p>
<ul>
<li>※新し目のMACで使えない</li>
<li>※USBドライブにアクセス制限あるPCはそれを解除する必要</li>
</ul>
</li>
<li><p>検知できると、こんな画面が出てくる</p>
</li>
</ul>
<img src="/images/2024/20240521a/image_11.png" alt="image.png" width="1200" height="748" loading="lazy">

<img src="/images/2024/20240521a/image_12.png" alt="image.png" width="1200" height="753" loading="lazy">

<ul>
<li>「Simulator」＞「Start Simulating」を押すと、Simulatorが立ち上がる<br>※ファイアウォールを無効化にする必要！ そうしないとRCからの信号が到達できない。</li>
</ul>
<img src="/images/2024/20240521a/image_13.png" alt="image.png" width="1200" height="750" loading="lazy">

<h3 id="１０．🍷🍷🍷操作できました🍷🍷🍷">１０．🍷🍷🍷操作できました🍷🍷🍷</h3><h2 id="結論">結論</h2><p>シミュレーターを動かすことができました。ドローンにUSBで繋ぐところはなぜファイアウォールを無効化する必要があるのか理解できていないため、今後の調査課題とします。</p>
<p>シミュレータで学んだ後は、ドローン本体を調達して動かしていきましょう！</p>
]]></content>
    <summary type="html">ドローンを利用して自動操縦（Autopilot）のロジックを組みたいですが、自宅でドローンを飛ばしたら家が壊されてしまう恐れがあるので、シミュレータを使います。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Android" scheme="https://future-architect.github.io/tags/Android/"/>
    <category term="エミュレータ" scheme="https://future-architect.github.io/tags/%E3%82%A8%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF/"/>
    <category term="ドローン" scheme="https://future-architect.github.io/tags/%E3%83%89%E3%83%AD%E3%83%BC%E3%83%B3/"/>
  </entry>
  <entry>
    <title>アプリストア規約を調査し、その内容をアプリへ反映する</title>
    <link href="https://future-architect.github.io/articles/20240520a/"/>
    <id>https://future-architect.github.io/articles/20240520a/</id>
    <published>2024-05-19T15:00:00.000Z</published>
    <updated>2024-05-19T15:00:00.000Z</updated>
    <author><name>李守智</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは、2014年フューチャーに新卒入社した李守智です。金融・アパレル・医療等分野で、システム構築系のプロジェクトに上流から下流まで一貫して携わってきました。</p>
<p>アプリ配信の前提となるアプリストア規約の調査とその結果をアプリへ反映する流れについて話します。このお話は、2021年ごろにスマホアプリ（Android、iOS）を一般配信するために行っていた経験を土台にしています。</p>
<h2 id="GoogleとAppleのアプリストア規約">GoogleとAppleのアプリストア規約</h2><p>どのアプリストアからアプリを配信するかは決まっていますか？ AndroidアプリはGoogle Play、iOSアプリはApp Storeからの配信が一般的ですよね。配信先ごとに規約を確認する必要があります。</p>
<p>開発が完了したアプリをGoogle Play ConsoleやApp Store Connectに登録したら、同じ規約に基づいてアプリストア側の審査を受けることになります。</p>
<p>Google Playのポリシー<br>https://play.google.com/intl/ja/about/developer-content-policy/</p>
<p>App Reviewガイドライン<br>https://developer.apple.com/jp/app-store/review/guidelines/</p>
<h2 id="アプリストア規約は誰がいつ確認するか">アプリストア規約は誰がいつ確認するか</h2><p>アプリストア規約は、デベロッパーが確認するものだけでなく、事業者が確認するものもあります。デベロッパーまたは事業者からアプリストア側へ問い合わせをする必要が出てくるようなものもあります。</p>
<p>プロジェクト開始／計画の段階で顧客とそれぞれの持分を認識合わせし、工数を確保しておくと良いでしょう。開発が完了してからやろうとすると、後述するような変更等から手戻りが発生する恐れがあります。アプリ配信の実績のない事業者の場合、アプリストア規約に違反するサービスを考えている可能性もあります。</p>
<p>また、規約は随時更新されているので、開発中はもちろん、運用保守の中でもウォッチできるようにしましょう。</p>
<h2 id="アプリストア規約はどのように確認するか">アプリストア規約はどのように確認するか</h2><p>当時私たちがやっていた方法を共有すると、規約の本文1文を1行のチェック項目とするチェックリストを作成して管理していました。</p>
<p>対象の規約、規約のバージョン、チェック項目（規約本文）、確認者、確認日、判定、コメント等を書き、チェックリストそのもののバージョン管理も行いました。</p>
<p>このチェックリストで判定OKとならなかったものが、課題チケットやタスクチケット等になっていきました。</p>
<img fetchpriority="high" src="/images/2024/20240520a/スクリーンショット_2024-05-16_142524.png" alt="" width="1200" height="422">

<h2 id="アプリストア規約がアプリ開発に影響を及ぼした例">アプリストア規約がアプリ開発に影響を及ぼした例</h2><h3 id="事例（1）アカウント削除機能の追加">事例（1）アカウント削除機能の追加</h3><p>App Reviewガイドラインによると、アカウント作成に対応したアプリの場合、ユーザ自身がそのアプリ内でアカウントを削除できるようにする必要があります。</p>
<p>例えばあるアプリはアカウントを作成してログインした状態での利用を前提とするものであるとします。もちろん、このケースではこの規約をクリアする必要があります。ここで問題になるのは、最小限の機能だけ実装したMVPをまずリリースしようとしたときで、アカウント削除機能は優先度的にスコープから漏れがちということです。実際にはファーストリリース時点で必須であるため、アカウント削除機能とその導線追加などをの考慮を、最初からスケジュールに組み込んでおきましょう。</p>
<p>機能定義前にこの対応が行われていたら手戻りは防げます。一方で、仮に開発完了後にこの対応をしようとすると、機能定義への追加はもちろん、アカウント削除機能への導線から発生する画面遷移の更新、開発・単体テスト、結合テストやシナリオテストなどなど、広範囲の変更が生じることになります。</p>
<h3 id="事例（2）ユーザーデータの使用制限">事例（2）ユーザーデータの使用制限</h3><p>ユーザーデータを収集するアプリの場合、ユーザーデータの使用をプライバシーポリシーに準拠した目的に制限する必要があります。これは、Google PlayのポリシーでもApp Reviewガイドラインでも謳われています。</p>
<p>この規約は、事例（1）とは違って、直感的にどんな対応をすれば良いか分かりにくいものでした。そこでまず、対応内容を決める前に以下のような思考の段階を経ました。</p>
<ol>
<li>システムの中のユーザーデータの出番を洗い出した上で</li>
<li>その必要性がプライバシーポリシーに準拠しているかの評価をする</li>
</ol>
<p>「プライバシーポリシーに準拠しているか」も、もう一段と解釈が必要かも知れません。プライバシーポリシーはユーザが承諾するアプリの利用規約の1つにもなるので、ざっくり表現すると「ユーザがその使い方をされると認識し、良いと了承している」ことと言えるかなと思います。</p>
<p>そうした検討結果としてユーザーデータのログ出力はプライバシーポリシーに準拠していないと自ら判断することもあります。例えばあるアプリで操作データを未加工でログ出力したとします。このケースでは、ユーザーデータが思わぬ使い方をされる可能性があります。なぜならログは容易に取得ができ、集計分析等が可能な一方で、その都度プライバシーポリシーに準拠した使い方がされることを、プロダクトのライフサイクルのすべての期間で保証することは難しいと想定できるからです。</p>
<p>そのため、運用保守の問い合わせ対応に必要な、ユーザを特定する情報以外のユーザーデータはマスキングしてログ出力するといった対応が必要です。もちろん、この対応をすると障害対応などでログを用いた追跡の難易度が上がります。そのため、プロジェクト計画の段階で対応時期等をチームで合意して進められるようにすることが重要です。</p>
<h3 id="その他の事例">その他の事例</h3><p>詳細は省略しますが、他にも「アプリを使い始めた後でも、ユーザが利用規約への同意を撤回できるようにする」とか、「iPhone向けのアプリがiPadでもうまく動作するか」など、対応検討の必要な規約は複数ありました。</p>
<h2 id="まとめ">まとめ</h2><p>どちらかというと、Google Playのポリシーよりも、App Reviewガイドラインからの方が、対応が難しい印象です。</p>
<p>規約の内容によって対応が簡単に決められるものもあれば、課題管理が必要なものもありました。アプリストア規約を基点とせずとも、プロジェクトのスコープに既に入っている対応もありました。いずれにせよ、品質の高いサービスを世の中に配信することにつながれば良いと思います。適時にアプリストア規約を調査し、プロジェクト品質に役立てましょう。</p>
]]></content>
    <summary type="html">アプリ配信の前提となるアプリストア規約の調査や、その結果をアプリへ反映する流れについてお話します。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Android" scheme="https://future-architect.github.io/tags/Android/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
    <category term="アプリストア審査" scheme="https://future-architect.github.io/tags/%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B9%E3%83%88%E3%82%A2%E5%AF%A9%E6%9F%BB/"/>
  </entry>
  <entry>
    <title>【iOS/Swift】ダークモード・ライトモード・端末の表示モードへアプリ内から自由に切り替える</title>
    <link href="https://future-architect.github.io/articles/20240430a/"/>
    <id>https://future-architect.github.io/articles/20240430a/</id>
    <published>2024-04-29T15:00:00.000Z</published>
    <updated>2024-04-29T15:00:00.000Z</updated>
    <author><name>橋本竜我</name></author>
    <content type="html"><![CDATA[<p>春の入門連載の14本目です。</p>
<h2 id="はじめに">はじめに</h2><p>HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>所属の橋本です。</p>
<p>みなさん、お使いのiPhoneではダークモードorライトモードどちらに設定していますか。</p>
<p>それとも、Automaticに設定していますか。</p>
<img fetchpriority="high" src="/images/2024/20240430a/46491578-7e42-ab93-f1eb-793c2d70c499.jpeg" alt="" width="863" height="721">

<p>私は、単純に黒っぽい画面にテンションが上がるタイプなので常時ダークモードで使っています。</p>
<p>目に優しいのはダークモードではなく、ライトモードみたいな記事も拝見したことがあり、一度ダークモードからライトモードに変更したこともありますが、結局ダークモードに落ち着いています。</p>
<p>そんなiPhoneにおけるダークモード・ライトモードの切り替えをアプリ内から行う方法の例を紹介したいと思います。</p>
<h2 id="環境">環境</h2><ul>
<li>OS：macOS Sonoma 14.4.1</li>
<li>Xcode：15.3 (15E204a)</li>
<li>Swift：5.10</li>
</ul>
<h2 id="ゴールイメージ">ゴールイメージ</h2><p>今回作成する表示モードをピッカーで選択できる設定画面です。</p>
<img src="/images/2024/20240430a/Simulator_Screen_Recording_-_iPhone_15_Pro_-_2024-04-12_at_23.16.27.gif" alt="Simulator_Screen_Recording_-_iPhone_15_Pro_-_2024-04-12_at_23.16.27.gif" width="240" height="520" loading="lazy">

<h2 id="実装方法">実装方法</h2><p>まず、切り替えたい３つの表示モードを列挙型で定義しておきます。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">DisplayMode</span>: <span class="title class_ inherited__">String</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> light</span><br><span class="line">    <span class="keyword">case</span> dark</span><br><span class="line">    <span class="keyword">case</span> system</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>続いて、アプリ全体に対して、表示モードを適用させたいと思います。</p>
<p>このときに、.preferredColorSchemeと三項演算子を用いて条件分岐します。<br><code>displayMode</code>が<code>.system</code>のときに、nilを返すことでシステムのデフォルトの設定を反映できるようにしています。また、<code>displayMode</code>が<code>.system</code>ではないときは、<code>diplayMode</code>が’.dark’であるときは、ColorSchemeの<code>.dark</code>が適用され、そうでないときは<code>.light</code>が適用されます。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">.preferredColorScheme(displayMode <span class="operator">==</span> .system <span class="operator">?</span> nil : (displayMode <span class="operator">==</span> .dark <span class="operator">?</span> .dark : .light))</span><br></pre></td></tr></table></figure>

<p>これをエントリーポイントである<code>〜App.swift</code>ファイルに次のように記載します。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> SwiftUI</span><br><span class="line"></span><br><span class="line"><span class="keyword">@main</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">displayModeApp</span>: <span class="title class_ inherited__">App</span> &#123;</span><br><span class="line">    <span class="meta">@AppStorage</span>(<span class="string">&quot;displayMode&quot;</span>) <span class="keyword">var</span> displayMode: <span class="type">DisplayMode</span> <span class="operator">=</span> .system</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">Scene</span> &#123;</span><br><span class="line">        <span class="type">WindowGroup</span> &#123;</span><br><span class="line">            <span class="type">ContentView</span>()</span><br><span class="line">                .preferredColorScheme(</span><br><span class="line">                    displayMode <span class="operator">==</span> .system <span class="operator">?</span> nil : (displayMode <span class="operator">==</span> .dark <span class="operator">?</span> .dark : .light)</span><br><span class="line">                )</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>最後に、<code>Picker</code>を用いて、表示モードを変更させるViewを作成します。</p>
<p><code>Picker</code>の<code>selection:</code>パラメータには<code>$displayMode</code>がバインドさせています。これは、選択された値が直接<code>displayMode</code>プロパティに保存されることを意味します。<br>それぞれの<code>tag()</code>メソッドに<code>DisplayMode</code>の対応する値が設定されています。これにより、ユーザーがピッカーで選択した表示モードが、<code>DisplayMode</code>型の<code>displayMode</code>に適切に保存することが可能になります。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">ContentView</span>: <span class="title class_ inherited__">View</span> &#123;</span><br><span class="line">    <span class="meta">@AppStorage</span>(<span class="string">&quot;displayMode&quot;</span>) <span class="keyword">var</span> displayMode: <span class="type">DisplayMode</span> <span class="operator">=</span> .system</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">NavigationStack</span> &#123;</span><br><span class="line">            <span class="type">List</span> &#123;</span><br><span class="line">                <span class="type">Section</span>(<span class="string">&quot;App Settings&quot;</span>) &#123;</span><br><span class="line">                    <span class="type">Picker</span>(<span class="string">&quot;Theme Color&quot;</span>, selection: <span class="variable">$displayMode</span>) &#123;</span><br><span class="line">                        <span class="type">Text</span>(<span class="string">&quot;Light&quot;</span>)</span><br><span class="line">                            .tag(<span class="type">DisplayMode</span>.light)</span><br><span class="line">                        <span class="type">Text</span>(<span class="string">&quot;Dark&quot;</span>)</span><br><span class="line">                            .tag(<span class="type">DisplayMode</span>.dark)</span><br><span class="line">                        <span class="type">Text</span>(<span class="string">&quot;System&quot;</span>)</span><br><span class="line">                            .tag(<span class="type">DisplayMode</span>.system)</span><br><span class="line">                    &#125;</span><br><span class="line">                    .pickerStyle(.automatic)</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            .navigationTitle(<span class="string">&quot;Settings&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>Done！</p>
<p>これでアプリ内から強制的に、ダークモード、ライトモードに変更させることができるようになりました。</p>
<p>触れていませんでしたが、@AppStoregeで端末内部に<code>DisplayMode</code>を記憶させているので、アプリキル後に立ち上げた際には、以前の設定値が保存されるようにもなっています。</p>
<h2 id="おわりに">おわりに</h2><p>ダークモード・ライトモード・端末の設定のモードにアプリ内から切り替える方法を扱いました。</p>
<p>このようにダークモード、ライトモードを変えること自体は簡単ですが、それぞれの色を各画面に対応させるほうが大変ですね。色の管理にはAsset Catalogを使うことでダークモード、ライトモードの色を設定しておく方法がありますが、このあたりについても、うまいやり方があればご紹介したいと思います。</p>
<h2 id="参考">参考</h2><ul>
<li>https://developer.apple.com/documentation/uikit/uisplitviewcontroller/1623194-displaymode</li>
<li>https://developer.apple.com/documentation/swiftui/view/preferredcolorscheme(_:)</li>
<li>https://developer.apple.com/documentation/swiftui/colorscheme</li>
</ul>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">みなさん、お使いのiPhoneではダークモードorライトモードどちらに設定していますか。それとも、Automaticに設定していますか。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="SwiftUI" scheme="https://future-architect.github.io/tags/SwiftUI/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
  </entry>
  <entry>
    <title>WSLで始めるサーバーサイドSwift 入門 ～Hummingbird～</title>
    <link href="https://future-architect.github.io/articles/20240418a/"/>
    <id>https://future-architect.github.io/articles/20240418a/</id>
    <published>2024-04-17T15:00:00.000Z</published>
    <updated>2024-04-17T15:00:00.000Z</updated>
    <author><name>清水雄一郎</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240418a/image.png" alt="image.png" width="851" height="200">

<h2 id="はじめに">はじめに</h2><p>こんにちは。HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>所属の清水です。<br>春の入門連載2024、7本目の記事です。</p>
<p>SwiftにおけるWebフレームワークを取り上げます。Swift<sup id="fnref:7">2</sup>はiOSアプリ開発のイメージが強いと思いますが、iOSアプリ開発以外でも利用できることを紹介します。<br>（macOS以外でもSwiftが動かせたら楽しいなというモチベーションで記事を執筆しております）。</p>
<h3 id="Swift-における-Web-フレームワーク">Swift における Web フレームワーク</h3><p>そもそもSwiftにおけるWebフレームワークは、何があるでしょうか？</p>
<p>Swift.org<sup id="fnref:8">3</sup>のSwift on Server内で紹介されているWeb フレームワークは以下2つです。</p>
<ul>
<li>Hummingbird</li>
<li>Vapor</li>
</ul>
<p>GitHub Star 数を比較した図を以下に示します。</p>
<p>先発のフレームワークであるVaporの方が人気で、検索すると日本語記事もいくつかヒットします。Swift.org内のWebフレームワークチュートリアル<sup id="fnref:2">4</sup>として取り上げられていることからも、Vaporが主流となっていることが伺えます。Vaporについては記事になっているものも多いため、後発のHummingbirdを試します。</p>
<img src="/images/2024/20240418a/star-history-2024416.png" alt="" width="1200" height="866" loading="lazy">
（参考：https://star-history.com/#hummingbird-project/hummingbird&vapor/vapor&Date）

<h3 id="Hummingbird-とは">Hummingbird とは</h3><p>軽量で依存関係が少ないことを売りとしているフレームワークの1つです。</p>
<p>Hummingbird の README にも以下の記載があります。</p>
<blockquote>
<p>Lightweight, flexible, modern server framework written in Swift.</p>
</blockquote>
<p>Hummingbird は、最小限のコアフレームワークのみを提供して、個別で拡張していくことができるようになっています。前述の Vapor の方が機能自体は豊富だと思いますが、サクッと使いたい場合は Hummingbird なのかなと認識しています。</p>
<p>また、公式ドキュメントがAppleのDeveloper向けのドキュメント<sup id="fnref:10">5</sup>そっくりなので、ぜひ覗いてみてほしいです。</p>
<p>https://docs.hummingbird.codes/2.0/documentation/hummingbird/</p>
<h3 id="セットアップ">セットアップ</h3><h4 id="環境情報">環境情報</h4><p>本記事は、以下の環境で実施を確認しています。</p>
<ul>
<li><p>Windows Subsystem for Linux （以下、WSL）に関するバージョン情報</p>
  <figure class="highlight console"><figcaption><span>cmd.exe</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">&gt;</span><span class="language-bash">wsl -v</span></span><br><span class="line">WSL バージョン: 2.1.5.0</span><br><span class="line">カーネル バージョン: 5.15.146.1-2</span><br><span class="line">WSLg バージョン: 1.0.60</span><br><span class="line">MSRDC バージョン: 1.2.5105</span><br><span class="line">Direct3D バージョン: 1.611.1-81528511</span><br><span class="line">DXCore バージョン: 10.0.25131.1002-220531-1700.rs-onecore-base2-hyp</span><br><span class="line">Windows バージョン: 10.0.19045.4170</span><br></pre></td></tr></table></figure>
</li>
<li><p>Ubuntu のバージョン: 22.04.4 LTS</p>
</li>
<li><p>Swift のバージョン: 5.10</p>
</li>
</ul>
<h4 id="Swift-のインストール">Swift のインストール</h4><p>まずは、SwiftをUbuntu上にインストールします。<br>Hummingbirdの公式ドキュメントにはSwift自体のインストールに関する記載はなかったため、Vaporの公式ドキュメントを参考にSwiftly<sup id="fnref:3">6</sup>を用いてインストールしました。<br>Swiftly は、Swift Server Workgroup<sup id="fnref:9">7</sup> が提供するCLIツールで異なるバージョンのSwiftを使い分けることができます。<br>記載の手順通り、実行していきます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">swiftly のインストール</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">curl -L https://swift-server.github.io/swiftly/swiftly-install.sh | bash</span></span><br><span class="line">~~~</span><br><span class="line">Select one of the following:</span><br><span class="line">1) Proceed with the installation (default)</span><br><span class="line">2) Customize the installation</span><br><span class="line">3) Cancel</span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">1</span></span><br><span class="line">~~~</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">`swiftly` を有効化するため、PATH へ追加</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">. <span class="variable">$HOME</span>/.local/share/swiftly/env.sh</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">最新バージョンの Swift をインストール</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">swiftly install latest</span></span><br><span class="line">~~~</span><br><span class="line">Set the active toolchain to Swift 5.10.0</span><br><span class="line">Swift 5.10.0 installed successfully!</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Swift がインストールされていることを確認する</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">swift --version</span></span><br><span class="line">Swift version 5.10 (swift-5.10-RELEASE)</span><br><span class="line">Target: x86_64-unknown-linux-gnu</span><br></pre></td></tr></table></figure>

<h3 id="Hummingbird-を用いたToDoアプリの構築（公式チュートリアル）">Hummingbird を用いたToDoアプリの構築（公式チュートリアル）</h3><p>ここから先は、Hummingbird 公式ドキュメント記載のチュートリアルを元に Hummingbird のセットアップからToDoアプリの構築までのハンズオンを試してみます。</p>
<p>https://docs.hummingbird.codes/2.0/tutorials/todos/</p>
<h4 id="Hummingbird-のセットアップ">Hummingbird のセットアップ</h4><figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">プロジェクトDirectoryの作成</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash"><span class="built_in">mkdir</span> Todos</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash"><span class="built_in">cd</span> Todos</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">パッケージに必要なテンプレートで初期化</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">swift package init --<span class="built_in">type</span> tool</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">ディレクトリ構成の確認</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash"> tree</span></span><br><span class="line">.</span><br><span class="line">├── Package.resolved # パッケージの依存関係解決を記録</span><br><span class="line">├── Package.swift # 依存パッケージを記載</span><br><span class="line">└── Sources</span><br><span class="line">    └── Todos.swift # 本体</span><br></pre></td></tr></table></figure>

<details><summary>--type tool について</summary>

<p><code>--type tool</code> は、コマンドライン引数をデフォルトで利用できるテンプレートみたいです。<br><code>Package.swift</code> に、 https://github.com/apple/swift-argument-parser.git が含まれるか否かと置き換えても問題ないと思います。</p>
<figure class="highlight console"><figcaption><span>--type executable の場合</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">swift package init --<span class="built_in">type</span> executable</span></span><br><span class="line">Creating executable package: Sample</span><br><span class="line">Creating Package.swift</span><br><span class="line">Creating .gitignore</span><br><span class="line">Creating Sources/</span><br><span class="line">Creating Sources/main.swift</span><br></pre></td></tr></table></figure>

<figure class="highlight swift"><figcaption><span>Package.swift</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// swift-tools-version: 5.10</span></span><br><span class="line"><span class="comment">// The swift-tools-version declares the minimum version of Swift required to build this package.</span></span><br><span class="line"><span class="keyword">import</span> PackageDescription</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> <span class="keyword">package</span> <span class="operator">=</span> <span class="type">Package</span>(</span><br><span class="line">    name: <span class="string">&quot;Sample&quot;</span>,</span><br><span class="line">    targets: [</span><br><span class="line">        <span class="comment">// Targets are the basic building blocks of a package, defining a module or a test suite.</span></span><br><span class="line">        <span class="comment">// Targets can depend on other targets in this package and products from dependencies.</span></span><br><span class="line">        .executableTarget(</span><br><span class="line">            name: <span class="string">&quot;Sample&quot;</span>),</span><br><span class="line">    ]</span><br><span class="line">)</span><br></pre></td></tr></table></figure>

</details>

<p>Hummingbird を利用するため、<code>Package.swift</code> に必要な情報を追加します。</p>
<figure class="highlight diff"><figcaption><span>Package.swift</span></figcaption><table><tr><td class="code"><pre><span class="line">// swift-tools-version: 5.10</span><br><span class="line">// The swift-tools-version declares the minimum version of Swift required to build this package.</span><br><span class="line">import PackageDescription</span><br><span class="line"></span><br><span class="line">let package = Package(</span><br><span class="line">    name: &quot;Todos&quot;,</span><br><span class="line"><span class="addition">++  // macOS 上で動かさないため、コメントアウトしているが問題なく動作した。</span></span><br><span class="line"><span class="addition">++  // platforms: [.macOS(.v14)],</span></span><br><span class="line">    dependencies: [</span><br><span class="line">        .package(url: &quot;https://github.com/apple/swift-argument-parser.git&quot;, from: &quot;1.2.0&quot;),</span><br><span class="line"><span class="addition">++      .package(url: &quot;https://github.com/hummingbird-project/hummingbird.git&quot;, from: &quot;2.0.0-alpha.1&quot;),</span></span><br><span class="line">    ],</span><br><span class="line">    targets: [</span><br><span class="line">        .executableTarget(</span><br><span class="line">            name: &quot;Todos&quot;,</span><br><span class="line">            dependencies: [</span><br><span class="line">                .product(name: &quot;ArgumentParser&quot;, package: &quot;swift-argument-parser&quot;),</span><br><span class="line"><span class="addition">++              .product(name: &quot;Hummingbird&quot;, package: &quot;hummingbird&quot;),</span></span><br><span class="line">            ]</span><br><span class="line">        ),</span><br><span class="line">    ]</span><br><span class="line">)</span><br></pre></td></tr></table></figure>

<p>GET リクエストを叩いた際に”Hello”を返すように以下のように <code>Todos.swift</code> を書き換えます。</p>
<figure class="highlight swift"><figcaption><span>Todos.swift</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> ArgumentParser</span><br><span class="line"><span class="keyword">import</span> Hummingbird</span><br><span class="line"></span><br><span class="line"><span class="keyword">@main</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Todos</span>: <span class="title class_ inherited__">AsyncParsableCommand</span> &#123;</span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">run</span>() <span class="keyword">async</span> <span class="keyword">throws</span> &#123;</span><br><span class="line">        <span class="comment">// create router</span></span><br><span class="line">        <span class="keyword">let</span> router <span class="operator">=</span> <span class="type">Router</span>()</span><br><span class="line">        <span class="comment">// add hello route</span></span><br><span class="line">        router.get(<span class="string">&quot;/&quot;</span>) &#123; request, context <span class="keyword">in</span></span><br><span class="line">            <span class="string">&quot;Hello<span class="subst">\n</span>&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// create application</span></span><br><span class="line">        <span class="keyword">let</span> app <span class="operator">=</span> <span class="type">Application</span>(router: router)</span><br><span class="line">        <span class="comment">// run application</span></span><br><span class="line">        <span class="keyword">try</span> <span class="keyword">await</span> app.runService()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>一連の修正が完了すると、アプリケーションが正常に動くことを確認できます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">swift run</span></span><br><span class="line">Building for debugging...</span><br><span class="line">[8/8] Linking Todos</span><br><span class="line">Build complete! (6.58s)</span><br><span class="line">2024-04-16T15:18:49+0900 info Hummingbird : [HummingbirdCore] Server started and listening on 127.0.0.1:8080</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">別タブで動作確認</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">curl localhost:8080</span></span><br><span class="line">Hello</span><br></pre></td></tr></table></figure>

<p>簡単ですね🎉</p>
<h4 id="ToDoアプリの構築">ToDoアプリの構築</h4><p>ここでは、Hummingbird を利用してToDoをDBに登録するところまで試してみます。<br>PostgresNIO<sup id="fnref:4">8</sup> というPostgreSQL用のSwiftクライアントを利用することで、ToDoをPostgreSQLに登録します。</p>
<p>Hummingbird 同様に、PostgresNIO を <code>Package.swift</code> に追加します。</p>
<figure class="highlight diff"><figcaption><span>Package.swift</span></figcaption><table><tr><td class="code"><pre><span class="line">// swift-tools-version: 5.10</span><br><span class="line">// The swift-tools-version declares the minimum version of Swift required to build this package.</span><br><span class="line">import PackageDescription</span><br><span class="line"></span><br><span class="line">let package = Package(</span><br><span class="line">    name: &quot;Todos&quot;,</span><br><span class="line">    // macOS 上で動かさないため、コメントアウトしているが問題なく動作した。</span><br><span class="line">    // platforms: [.macOS(.v14)],</span><br><span class="line">    dependencies: [</span><br><span class="line">        .package(url: &quot;https://github.com/apple/swift-argument-parser.git&quot;, from: &quot;1.2.0&quot;),</span><br><span class="line">        .package(url: &quot;https://github.com/hummingbird-project/hummingbird.git&quot;, from: &quot;2.0.0-alpha.1&quot;),</span><br><span class="line"><span class="addition">++      .package(url: &quot;https://github.com/vapor/postgres-nio.git&quot;, from: &quot;1.21.0&quot;),</span></span><br><span class="line">    ],</span><br><span class="line">    targets: [</span><br><span class="line">        .executableTarget(</span><br><span class="line">            name: &quot;Todos&quot;,</span><br><span class="line">            dependencies: [</span><br><span class="line">                .product(name: &quot;ArgumentParser&quot;, package: &quot;swift-argument-parser&quot;),</span><br><span class="line">                .product(name: &quot;Hummingbird&quot;, package: &quot;hummingbird&quot;),</span><br><span class="line"><span class="addition">++              .product(name: &quot;PostgresNIO&quot;, package: &quot;postgres-nio&quot;),</span></span><br><span class="line">            ]</span><br><span class="line">        ),</span><br><span class="line">    ]</span><br><span class="line">)</span><br></pre></td></tr></table></figure>

<p>PostgreSQL をインストールします。<br>チュートリアルでは（おそらくmacOS前提で） <code>brew</code> 経由でインストールしていたのですが、今回は <code>apt</code> 経由でインストールします。</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>Linux 上でも Homebrew インストールできるみたいなので、Homebrew 経由でインストールでも良いかもしれません。<br>https://docs.brew.sh/Homebrew-on-Linux</p>
</div></div>

<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash"><span class="built_in">sudo</span> apt update</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash"><span class="built_in">sudo</span> apt install postgresql postgresql-contrib</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">psql --version</span></span><br><span class="line">psql (PostgreSQL) 14.11 (Ubuntu 14.11-0ubuntu0.22.04.1)</span><br></pre></td></tr></table></figure>

<p>インストールが完了したので、チュートリアルの続きを実施します。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">psql postgres</span></span><br><span class="line">psql: error: connection to server on socket &quot;/var/run/postgresql/.s.PGSQL.5432&quot; failed: No such file or directory</span><br><span class="line">        Is the server running locally and accepting connections on that socket?</span><br></pre></td></tr></table></figure>

<p>エラーで進めません。<br>エラーについて調べると <code>.conf</code> ファイルを確認しよう<sup id="fnref:5">9</sup>、などとありますが、そもそも<code>.conf</code>ファイルを含んだディレクトリ（<code>/etc/postgresql/14/main/</code>）がないことに気が付きました。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash"><span class="built_in">ls</span> -la /etc/postgresql/</span></span><br><span class="line">total 8</span><br><span class="line">drwxr-xr-x  2 postgres postgres 4096 Feb 10  2022 .</span><br><span class="line">drwxr-xr-x 84 root     root     4096 Apr 17 06:54 ..</span><br></pre></td></tr></table></figure>

<p>別の記事<sup id="fnref:6">10</sup>を参考に、PostgreSQL のクラスタを追加します。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash"><span class="built_in">sudo</span> pg_createcluster --start 14 main</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash"><span class="built_in">ls</span> -la /etc/postgresql/14/main/</span></span><br><span class="line">conf.d/          pg_ctl.conf      pg_ident.conf    start.conf</span><br><span class="line">environment      pg_hba.conf      postgresql.conf</span><br></pre></td></tr></table></figure>

<p>動くようになったので、チュートリアルの続きに戻ります。<br>まずは、データベースを準備します。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">postgres psql</span></span><br><span class="line">psql (14.11 (Ubuntu 14.11-0ubuntu0.22.04.1))</span><br><span class="line">Type &quot;help&quot; for help.</span><br><span class="line"></span><br><span class="line">postgres=# create database hummingbird;</span><br><span class="line">CREATE DATABASE</span><br><span class="line">postgres=# \c hummingbird</span><br><span class="line">You are now connected to database &quot;hummingbird&quot; as user &quot;postgres&quot;.</span><br><span class="line">hummingbird=# create role todos createrole login password &#x27;todos&#x27;;</span><br><span class="line">CREATE ROLE</span><br><span class="line">hummingbird=# \q</span><br></pre></td></tr></table></figure>

<p>次に、ToDoアプリの実装です。<br>チュートリアルの通りに実装していきます。完成形は、公式のサンプルがあるため、そちらをご確認ください。</p>
<p>https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-postgres-tutorial</p>
<h4 id="動作確認">動作確認</h4><p>これまで構築したものの動作確認を行います。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">swift run</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">別タブで動作確認</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">TODO を登録</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">curl -i -X POST localhost:8080/todos -d<span class="string">&#x27;&#123;&quot;title&quot;: &quot;Wash my hair&quot;&#125;&#x27;</span></span></span><br><span class="line">HTTP/1.1 201 Created</span><br><span class="line">Content-Type: application/json; charset=utf-8</span><br><span class="line">Content-Length: 145</span><br><span class="line">Date: Tue, 16 Apr 2024 23:54:54 GMT</span><br><span class="line"></span><br><span class="line">&#123;&quot;id&quot;:&quot;A4672369-753C-49AD-A41C-849AE5A7CF1E&quot;,&quot;url&quot;:&quot;http:\/\/localhost:8080\/todos\/A4672369-753C-49AD-A41C-849AE5A7CF1E&quot;,&quot;title&quot;:&quot;Wash my hair&quot;&#125;</span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">curl -i -X POST localhost:8080/todos -d<span class="string">&#x27;&#123;&quot;title&quot;: &quot;Brush my teeth&quot;&#125;&#x27;</span></span></span><br><span class="line">HTTP/1.1 201 Created</span><br><span class="line">Content-Type: application/json; charset=utf-8</span><br><span class="line">Content-Length: 147</span><br><span class="line">Date: Tue, 16 Apr 2024 23:55:03 GMT</span><br><span class="line"></span><br><span class="line">&#123;&quot;url&quot;:&quot;http:\/\/localhost:8080\/todos\/435A9B4F-BD4A-42E2-8EA9-5A5560378BBA&quot;,&quot;id&quot;:&quot;435A9B4F-BD4A-42E2-8EA9-5A5560378BBA&quot;,&quot;title&quot;:&quot;Brush my teeth&quot;&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">DB の登録状態を確認</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">psql hummingbird</span></span><br><span class="line">psql (14.11 (Ubuntu 14.11-0ubuntu0.22.04.1))</span><br><span class="line">Type &quot;help&quot; for help.</span><br><span class="line"></span><br><span class="line">hummingbird=# select * from todos;</span><br><span class="line">                  id                  |     title      | order | completed |                               url</span><br><span class="line">--------------------------------------+----------------+-------+-----------+------------------------------------------------------------------</span><br><span class="line"> a4672369-753c-49ad-a41c-849ae5a7cf1e | Wash my hair   |       |           | http://localhost:8080/todos/A4672369-753C-49AD-A41C-849AE5A7CF1E</span><br><span class="line"> 435a9b4f-bd4a-42e2-8ea9-5a5560378bba | Brush my teeth |       |           | http://localhost:8080/todos/435A9B4F-BD4A-42E2-8EA9-5A5560378BBA</span><br><span class="line">(2 rows)</span><br></pre></td></tr></table></figure>

<p>POSTしたToDoが、DB側に登録されていることを確認できました🎉</p>
<h2 id="さいごに">さいごに</h2><p>Swift の Web フレームワークの1つである、Hummingbird をセットアップから紹介しました。</p>
<p>個人的には、macOS以外でもSwiftが書けて、かつ、サーバーアプリ開発に利用できることが知れて楽しかったです。WSL 上でセットアップしたので手順が多めだったのですが、普段SwiftでiOSアプリ開発している方がサーバサイドの開発を試したいときに便利だと思いました。</p>
<p>今回は試せませんでしたが、Xcode のシミュレータとバックエンドの接続までをどこかで試してみたいと考えています。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。
設立エピソードは、以下記事をご覧ください。
&quot;新規事業の立ち上げ　フューチャーの知られざる医療・ヘルスケアへの挑戦&quot;</span> ↩</li><li id="fn:7"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">https://www.apple.com/jp/swift/</span> ↩</li><li id="fn:8"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;">https://www.swift.org/</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">4.</span><span style="vertical-align: top;">Swift.org内のチュートリアル記事
https://www.swift.org/getting-started/vapor-web-server/</span> ↩</li><li id="fn:10"><span style="vertical-align: top; padding-right: 10px;">5.</span><span style="vertical-align: top;">AppleのDeveloper向けのドキュメント
https://developer.apple.com/documentation/Swift</span> ↩</li><li id="fn:3"><span style="vertical-align: top; padding-right: 10px;">6.</span><span style="vertical-align: top;">swiftly
https://github.com/swift-server/swiftly</span> ↩</li><li id="fn:9"><span style="vertical-align: top; padding-right: 10px;">7.</span><span style="vertical-align: top;">Swift によるサーバーアプリケーション開発を促進するワークグループ
https://www.swift.org/sswg/</span> ↩</li><li id="fn:4"><span style="vertical-align: top; padding-right: 10px;">8.</span><span style="vertical-align: top;">Postgres-NIO
https://github.com/vapor/postgres-nio</span> ↩</li><li id="fn:5"><span style="vertical-align: top; padding-right: 10px;">9.</span><span style="vertical-align: top;">psql: error: connection to server on socket &quot;/var/run/postgresql/.s.PGSQL.5432&quot; failed: FATAL: Peer authentication failed for user &quot;postgres&quot; (Ubuntu)
https://stackoverflow.com/questions/69676009/psql-error-connection-to-server-on-socket-var-run-postgresql-s-pgsql-5432</span> ↩</li><li id="fn:6"><span style="vertical-align: top; padding-right: 10px;">10.</span><span style="vertical-align: top;">【Ubuntu+PostgreSQL】postgres以外のユーザでクラスタを作成して起動する
https://qiita.com/shin4488/items/175151e59a043c724b38</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">SwiftにおけるWebフレームワークを取り上げます。SwiftはiOSアプリ開発のイメージが強いと思いますが、iOSアプリ開発以外でも利用できることを紹介します。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="Ubuntu" scheme="https://future-architect.github.io/tags/Ubuntu/"/>
    <category term="WSL" scheme="https://future-architect.github.io/tags/WSL/"/>
  </entry>
  <entry>
    <title>【Firebase】GDG Tokyo Monthly Online Tech Talksに登壇しました</title>
    <link href="https://future-architect.github.io/articles/20240221a/"/>
    <id>https://future-architect.github.io/articles/20240221a/</id>
    <published>2024-02-20T15:00:00.000Z</published>
    <updated>2024-02-20T15:00:00.000Z</updated>
    <author><name>山本竜玄</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240221a/image.png" alt="image.png" width="660" height="270">

<h2 id="はじめに">はじめに</h2><p>こんにちは。最近スギ花粉耐性がないことを実感しつつある山本です。</p>
<p>2024&#x2F;2&#x2F;1、GDG Tokyo主催の「【Firebase】GDG Tokyo Monthly Online Tech Talks」に「Flutter×Firebaseサービス達で高速でモバイルアプリを開発した話」というタイトルで登壇してきたので、その事後レポートです。</p>
<p>登壇資料：</p>


<h2 id="GDG-Tokyo-Monthly-Online-Tech-Talksとは？">GDG Tokyo Monthly Online Tech Talksとは？</h2><blockquote>
<p>Google Developers Group (GDG) Tokyo は主にGoogleのテクノロジーに興味のある人たちで情報を共有しあう集いです</p>
</blockquote>
<blockquote>
<p>GDG Tokyoが毎月開催する「GDG Tokyo Monthly Online Tech Talks」は、Googleの技術に関心を持つ人々が集うオンラインMeetupです。このイベントは、Android、Google Cloud、Web、Firebase、Machine Learning（ML）、Flutter、Goなど、多様なGoogleの技術に焦点を当てています。参加者は、技術情報をキャッチアップし、エンジニア同士のコミュニケーションと交流の場としてご活用いただけます。</p>
</blockquote>
<p>(※GDG Tokyoのconnpassページより引用)</p>
<hr>
<p>「GDG Tokyo」および「GDG Tokyo Monthly Online Tech Talks」についてはGDG Tokyoのグループの説明として上記が記載されています。</p>
<p>今回のイベントのテーマは「Firebase」に関わることで、15分枠 or 5分枠の登壇枠で開催されていました。</p>
<h2 id="登壇">登壇</h2><p>最近お仕事でFirebaseに携わっていたこともあり、社内の人におすすめされたので登壇を申し込んでみました。(会社アカウントとしては初の登壇です…！ )</p>
<p>登壇内容としては、新規事業プロジェクト周りでFlutterとFirebaseを活用していたのでそのユースケースの紹介をしてみました。</p>
<p>以下内容を一部抜粋して紹介していきます。</p>
<h2 id="アーキテクチャ紹介">アーキテクチャ紹介</h2><p>今回紹介したユースケースとしてはモバイルアプリ側はFlutter、バックエンドサービス側をFirebaseのサービスを活用したものです。</p>
<img src="/images/2024/20240221a/image_2.png" alt="image.png" width="962" height="540" loading="lazy">

<p>どちらもGoogle製ということもあり、親和性がある&amp;ドキュメントが豊富ということでこのアーキテクチャが選定されています。</p>
<img src="/images/2024/20240221a/image_3.png" alt="image.png" width="961" height="543" loading="lazy">

<p>使用したサービスを一覧で並べてみると、Firebaseを主としたかなりモダンな構成と言えるのではないでしょうか？<br>自分は開発チームにあとから参画したのですが、なかなか良い経験となっています。</p>
<h2 id="ログ-アラートについて">ログ&amp;アラートについて</h2><img src="/images/2024/20240221a/image_4.png" alt="image.png" width="963" height="536" loading="lazy">

<p>Firebaseでログ・アラート周りに使用できるサービスとしては以下の2つがあります。</p>
<ul>
<li>Firebase Crashlytics</li>
<li>Google Analytics for Firebase</li>
</ul>
<p>構成としてはいろいろなものが考えられますが、今回はCrashlyticsについては主にクラッシュor致命的なエラーを通知するように、Google Analyticsにはユーザーログを出したりBigQueryに連携したりしてユーザーの操作の追跡や広告効果の測定といったことを行っていました。</p>
<p>※BigQueryへの連携については松井さんが以下記事で解説してます。</p>
<p>Firebaseで取得したログをBigQueryに連携してユーザー操作をトラッキングする</p>
<h2 id="Firebase-Remote-Configについて">Firebase Remote Configについて</h2><img src="/images/2024/20240221a/image_5.png" alt="image.png" width="964" height="538" loading="lazy">

<p>その他に活用していてユニークだったサービスとしては、Remote Configがあります。</p>
<p>https://firebase.google.com/docs/remote-config?hl=ja</p>
<p>詳しくは上記の公式ページを見ていただきたいですが、Remote Config Serverでパラメータ値などを管理することで、アプリストアへのリリースすることなくユーザーアプリの画面切り替えや強制アップデートなども行えます。</p>
<p>まだ導入できていませんが、A&#x2F;B testingなども行うことができるようなので将来的には触ってみたいですね。</p>
<h2 id="まとめ">まとめ</h2><p>今回はGDG Tokyoにて、最近触ったFirebaseサービスとFlutterの活用事例について登壇してきました！<br>会社アカウントで登壇することは初めてだったのですが、登壇になれた方やチームメンバーに手厚くフォロー頂き楽しく発表してくることができました。</p>
<p>今後もどんどん登壇やアウトプットできるようにがんばります。<br>※スギ花粉とも頑張って生きていこうと思います。</p>
<h2 id="参考文献">参考文献</h2><ul>
<li>https://firebase.google.com/docs/remote-config?hl=ja</li>
</ul>
]]></content>
    <summary type="html">2024/2/1、GDG Tokyo主催の「【Firebase】GDG Tokyo Monthly Online Tech Talks」に「Flutter×Firebaseサービス達で高速でモバイルアプリを開発した話」というタイトルで登壇しました</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Firebase" scheme="https://future-architect.github.io/tags/Firebase/"/>
    <category term="Flutter" scheme="https://future-architect.github.io/tags/Flutter/"/>
    <category term="GDG" scheme="https://future-architect.github.io/tags/GDG/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    <category term="登壇レポート" scheme="https://future-architect.github.io/tags/%E7%99%BB%E5%A3%87%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
  </entry>
  <entry>
    <title>時を駆けるモバイルアプリUI設計: 2007-2023の理論とトレンドを調べてみた</title>
    <link href="https://future-architect.github.io/articles/20231031a/"/>
    <id>https://future-architect.github.io/articles/20231031a/</id>
    <published>2023-10-30T15:00:00.000Z</published>
    <updated>2023-10-30T15:00:00.000Z</updated>
    <author><name>山本竜玄</name></author>
    <content type="html"><![CDATA[<p>※この記事は、秋のブログ週間2023の２本目の記事となります。</p>
<h2 id="はじめに">はじめに</h2><p>2023年の秋、スマートフォンやタブレットは私たちの日常生活に欠かせない存在となっています。</p>
<p>これらのデバイスに対するアプリケーション開発の中でも、UI(User Interface)とUX(User Experience)設計の議論については欠かすことができません。</p>
<p>「UI」という単語は広範囲にわたる意味を持ちますが、今回は「モバイルアプリのUI設計」という文脈に焦点を絞り、その理論の歴史とトレンドについて調べてみました。</p>
<h2 id="モバイルアプリのUI設計について">モバイルアプリのUI設計について</h2><p>優れた「モバイルアプリのUI」は、どのようなものがあるでしょうか？</p>
<p>イメージしやすいものだと、以下のようなものがあります。</p>
<ul>
<li>ボタンの色や配置がわかりやすい。ホームボタンであれば家の形をしていたり、検索ボタンでは虫眼鏡の形をしているなど</li>
<li>タップしたときに何が起こるのか、ユーザーにわかりやすいこと。アプリヘッダーに「←」(左矢印)が表示されていれば、前の画面に戻るなど</li>
<li>通信が発生するボタンを押したときに、スピナー(通常は回転するアイコン)が表示され、かつ短時間で処理が終わり待ち時間が少ないこと</li>
</ul>
<p>逆に、優れていないUIについては以下のような報告もあります。</p>
<ul>
<li>モバイルショッピングアプリのUIの使いやすさがユーザーの購買意欲に直接影響するといったことを示唆する研究</li>
<li>ユーザーの90％がパフォーマンスの低下でアプリの使用を停止したとする調査レポート</li>
</ul>
<p>せっかく良い内容のアプリを作っても、UIがその価値を下げては台無しです。</p>
<p>では、良いUIとはどのようなものでしょうか？ その設計とは？</p>
<p>次の項からは、モバイルアプリのUI設計の理論や思想について、どのようなものがあるのか。また、その歴史について書籍や論文などから調べてみました。</p>
<h2 id="モバイルアプリUI設計理論・思想の歴史">モバイルアプリUI設計理論・思想の歴史</h2><p>モバイルアプリのUI設計の歴史を振り返ると、2007年のApple初代iPhoneの発表が大きなマイルストーンとして挙げられます。</p>
<p>それ以前にも、モバイルデバイスは存在していました。パーソナルデジタルアシスタント（PDA）やタッチスクリーン携帯電話と呼ばれるようなものです。これらのデバイスの中にはタッチスクリーンを備えたものもありましたが、多くはスタイラスによって操作されるもので、指タッチによる操作はあまり前提としていないものが多かったようです。</p>
<p>初代iPhoneでは、現在ではモバイルデバイス操作の標準となっている指タッチでの操作が導入され、それが世界的な流行を引き起こしました。これにより、モバイルデバイスの操作方法は劇的に変化し、モバイルアプリのUI設計についても新たな時代を迎えました。</p>
<p>そこからも、マルチタッチ技術の進化、Android,iOSなどのプラットフォームの進化、折りたたみデバイスの登場、あるいはユーザー自体の期待の向上など、様々な変化が起こってきました。それらに応じるように、近年のモバイルアプリのUI設計に至るまで、様々な理論や思想が議論され、そして進化し続けています。</p>
<p>以下では、今回調べたモバイルアプリUI設計理論や思想に関わる書籍・論文について、いくつかピックアップして時系列順に紹介していきます。</p>
<ol>
<li>Human Interface Guidelines(1977-現在)</li>
<li>Mobile Design and Development: Practical concepts and techniques for creating mobile sites and web apps(2009)</li>
<li>Brave NUI World: Designing Natural User Interfaces for Touch and Gesture by Daniel Wigdor and Dennis Wixon (2011)</li>
<li>Mobile First(2011)</li>
<li>Microinteractions: Designing with Details (2013)</li>
<li>Material Design (2014)</li>
</ol>
<p>※総務省の調査によると、日本のスマートフォン普及率は以下のような推移をたどっているようです。<br>時系列と普及率を見比べてご覧いただければと思います。</p>
<img fetchpriority="high" src="/images/2023/20231031a/image.png" alt="image.png" width="875" height="720">
[総務省「通信利用動向調査」より引用](https://www.soumu.go.jp/johotsusintokei/statistics/statistics05.html)

<h3 id="1-Human-Interface-Guidelines-1977-現在">1. Human Interface Guidelines(1977-現在)</h3><p>https://developer.apple.com/design/human-interface-guidelines</p>
<p>時系列順に紹介すると、最初に来るのはAppleが提唱した「HumanInterface Guidelines(HIG)」です。こちらは、Appleが定期的にアップデートをしているインターフェースのガイドラインで、歴史を遡ると初版である「The Apple II Human Interface Guidelines」は1977年のものとなります。</p>
<p>とはいえ、「Macintosh GUI」についての原則と設計哲学が記載されたものとなり、モバイルデバイスに関わるものとしては、「iPhone Human Interface Guidelines」が初代iPhoneの発売と同年である2007年に公表されています。</p>
<p>このHIGは現在に至るまで様々な製品、OSのアップデートに伴い改定され続けており、iOSの普及や統一されたインターフェースに大きな貢献をしているのではないでしょうか？</p>
<p>※余談ですが、弊社にもHIG(Healthcare Innovation Group)があって時々まぎらわしかったりします。</p>
<h3 id="2-Mobile-Design-and-Development-Practical-concepts-and-techniques-for-creating-mobile-sites-and-web-apps-2009">2. Mobile Design and Development: Practical concepts and techniques for creating mobile sites and web apps(2009)</h3><p>https://www.oreilly.com/library/view/mobile-design-and/9780596806231/</p>
<p>次に紹介するのはBrian Flingによる執筆、O’Reilly Mediaから出版されている「Mobile Design and Development」です。この書籍では、モバイルアプリの設計や開発について網羅的に解説されています。</p>
<p>目次としては、以下のようになっており、記載当時のモバイルアプリ開発の状況についてや、モバイルUI設計の状況について知る上でとてもわかりやすいものでした。</p>
<blockquote>
<p>Chapter 1, A Brief History of Mobile<br>Chapter 2, The Mobile Ecosystem<br>Chapter 3, Why Mobile?<br>Chapter 4, Designing for Context<br>Chapter 5, Developing a Mobile Strategy<br>Chapter 6, Types of Mobile Applications<br>Chapter 7, Mobile Information Architecture<br>Chapter 8, Mobile Design<br>Chapter 9, Mobile Web Apps Versus Native Applications<br>Chapter 10, Mobile 2.0<br>Chapter 11, Mobile Web Development<br>Chapter 12, iPhone Web Apps<br>Chapter 13, Adapting to Devices<br>Chapter 14, Making Money in Mobile<br>Chapter 15, Supporting Devices<br>Chapter 16, The Future of Mobile</p>
</blockquote>
<p>目次を見るだけでも、当時のモバイルアプリ開発としてはネイティブアプリが絶対的な選択肢とはならず、Webアプリとどちらを選択するのかといったことも大きな議論の対象のひとつであったことが伺えます。</p>
<p>また、この書籍だけでなく、2011年ごろの論文6,7でも、ネイティブアプリ vs ウェブアプリといった文脈で盛んに議論がかわされていたようです。</p>
<p>2023年の現時点においても、ネイティブアプリとWebページをネイティブアプリのように表示できるPWA(Progressive Web Apps)の比較について議論されることがありますが、当時の状況では議論の内容が少々異なったようです。</p>
<blockquote>
<p>The mobile web historically has been so horrendous to deal with that<br>  the only way to create a compelling experience was to go native.<br>(p.156を引用)</p>
</blockquote>
<p>本書では上記で記載されているように、筆者によるとモバイルWebアプリケーションを開発すること自体に一定のハードルがあったようです。</p>
<p>背景として、当時のMobile SafariではCSSの<code>position:fixed</code>の動作に課題があったり8、特定のOS標準のブラウザでは描画に課題があったり9したように、モバイルブラウザ自体の技術が成熟していなかったことが考えられます。</p>
<p>次に、本記事のテーマであるUIについて触れられている項を見てみましょう。</p>
<blockquote>
<p>Although information architecture has become a common discipline in<br>    the web industry, unfortunately, the mobile industry—like software—has<br>    only a handful of specialized mobile information architects. Although<br>    mobile information architecture is hardly a discipline in its own right,<br>    it certainly ought to be. This is not because it is so dissimilar from its<br>    desktop cousin, but because of context, added technical constraints, and<br>    needing to display on a smaller screen as much information as we would on<br>    a desktop.<br>(p.104を引用)</p>
</blockquote>
<p>とあるように、モバイルに限らないUI&#x2F;UXといった議論は行われていたようですが10、モバイルアプリのUI設計に特異な議論はまだ主流でなかったことが考えられます。</p>
<p>本書においては、「Chapter 7, Mobile Information Architecture」のセクションでは、モバイルアプリの情報アーキテクトとして、サイトマップやクリックストリームについて記載されています。既存のWebサイト構築とは異なるTipsとして、ユーザーの操作タスクが短いことがあげられています。</p>
<p>また、2023年と異なる興味深い点として、間違ったページに遷移しやすい構成である場合にユーザーに与える悪影響として以下が挙げられています。</p>
<ul>
<li>ユーザーが前のページに迅速に戻るための信頼性のあるブロードバンド接続を持っているとは限らないことを想定する必要があること</li>
<li>通信が発生することで、通信料金が発生すること</li>
</ul>
<p>「Chapter 8, Mobile Design」のセクションでは、モバイルアプリのデザイン構築についてのTipsが網羅的にわかりやすく記載されていました。特に、モバイルデバイスは通常のWebサイトとは違い、画面が小さいことを意識することの重要性について述べられている部分が多かったように感じます。</p>
<p>今ではResponsible Designと呼ばれる概念に近いですが、「Responsive Web Design」という単語自体が2010年にEthan Marcotteによって提唱されたものだそうで6、記事執筆時に単語としては普及していなかった可能性もあります。</p>
<p>モバイルデバイスも考慮したレイアウトの議論についても、盛んに行われていたようです。</p>
<h3 id="3-Brave-NUI-World-Designing-Natural-User-Interfaces-for-Touch-and-Gesture-by-Daniel-Wigdor-and-Dennis-Wixon-2011">3. Brave NUI World: Designing Natural User Interfaces for Touch and Gesture by Daniel Wigdor and Dennis Wixon (2011)</h3><p>https://www.amazon.co.jp/Brave-NUI-World-Designing-Interfaces/dp/0123822319</p>
<p>次に紹介するのはDaniel Wigdorによる執筆、Morgan Kaufmannから出版されている「Brave NUI World」です。この書籍では、タッチとジェスチャーを中心としたナチュラルユーザーインターフェース(NUI)の設計について記載されています。</p>
<p>NUI自体は、モバイルデバイスの文脈に閉じる話ではなく、あらゆる入力テクノロジーを活用して構築された自然なインターフェースに意味が及びます。ですが、本書では特にタッチとジェスチャーの相互作用に重点が置かれて紹介されており、この時期に盛んとなっているタッチジェスチャーに関する文献の中でも著名なものであったので本記事でピックアップします。</p>
<p>目次としては、以下のようになっています。(なかなかボリュームある本でした)</p>
<blockquote>
<p>PartⅠ Introducing the NUI<br>CHAPTER 1 Introduction<br>CHAPTER 2 The Natural User Interface<br>CHAPTER 3 Ecological Niche: Computing, the Social Environment, and Ways of Working<br>Part Ⅱ Design Ethos of NUI<br>CHAPTER 4 Less Is More<br>CHAPTER 5 Contextual Environments<br>CHAPTER 6 The Spatial NUI<br>CHAPTER 7 The Social NUI<br>CHAPTER 8 Seamless<br>CHAPTER 9 Super Real<br>CHAPTER 10 Scaffolding<br>CHAPTER 11 User Defferentiation<br>Part Ⅲ NewTechnologies: Understanding and Technological Artifacts<br>CHAPTER 12 The State-Transition Model Of Input<br>CHAPTER 13 Fat Fingers<br>CHAPTER 14 No Touch Left Behind: Feedback Is Essential<br>CHAPTER 15 Touch versus In-Air Gestures<br>Part Ⅳ Creating an Interaction Language<br>CHAPTER 16 Mechanics, Dynamics, and Aesthetics: The Application of MDA<br>CHAPTER 17 New Primitives<br>CHAPTER 18 The Anatomy of a Gesture<br>CHAPTER 19 Properties of a Gesture Language<br>CHAPTER 20 Self-Revealing Gesture<br>CHAPTER 21 A Model of the Mode and Flow of a Gesture System<br>Part Ⅴ No Such Thing as Touch<br>CHAPTER 22 Know Your Platform<br>CHAPTER 23 The Fundamentals Have to Work<br>CHAPTER 24 Numbers of Contents<br>CHAPTER 25 Contact Data: Shape, Pressure, and Hover<br>CHAPTER 26 Vertical, Horizontal, and Mobile<br>Part Ⅵ Process: How Do You Get There?<br>CHAPTER 27 The User-Derived Interface(UDI)<br>CHAPTER 28 Lessons in False-Gesture Recognition<br>CHAPTER 29 RITE with a Purpose<br>CHAPTER 30 A Word About Engineering</p>
</blockquote>
<p>本書では、GUIを中心とした既存のマウス入力と、タッチ入力を中心としたデバイス操作には大きな違いがあることを認識した上で設計をする必要性を強く主張しています。</p>
<p>具体的なタッチとジェスチャーに関する項目はPart Ⅲ以降から様々な角度、考え方から科学的に記載をされています。</p>
<p>全編通して参考になる本でしたが、1つ興味深い例を上げるとするのであれば「CHAPTER 13 Fat Fingers」が挙げられます。このセクションでは、指タッチで生じる課題として、（1）指はマウスよりも大きな領域で接触するため、画面のピクセル単位での正確なタッチが難しいこと（2）タッチしている箇所が指自体で見えなくなること、を課題として挙げています。言われてみれば当然と思う方もいるかもしれませんが、自身としてはUIを考える上であまり意識していなかった部分であったので興味深かったです。</p>
<p>この課題への対応として、（1）タッチオブジェクトを十分に大きくすることや、（2）「Iceberg Targets」と呼ばれる、画面領域をいくつかに分割し、タッチポイントに最も近いオブジェクトに対してアクションを発生させること、などが記載されていました。</p>
<p>現在のモバイルデバイスでは一般的となっていて、ユーザーとしても慣れ親しんでいるタッチ入力ですが、タッチ、タップ、フリック、ピンチ操作など、基本的な操作方法としては大きな変化はないように感じます。そういった意味でも、本書はそれらの基本操作をいかにユーザーに自然に操作できるように設計できるかという部分で大きく勉強になる本でした。</p>
<p>この書籍と同時期に、タッチジェスチャーに関する研究・書籍についても多く出ているようです。iPad (第1世代)も本書の出版と同年の2010年に発売され、他にもタッチスクリーン採用のモバイルデバイスが次々発売されていた時期のようで、タッチ入力がより普及していたことが伺えます。</p>
<h3 id="4-Mobile-First-2011">4. Mobile First(2011)</h3><p>https://www.amazon.co.jp/Mobile-First-Luke-Wroblewski/dp/1937557022</p>
<p>次に紹介するのは、Luke Wroblewskiによる執筆、Ingramから出版された「Mobile First」です。統計的にはモバイルデバイスの普及が爆発的に始まった2010年近辺の出版であり、いよいよモバイルアプリの立ち位置が向上したことを思わせるタイトルです。</p>
<p>この書籍の内容としては、タイトル通り「モバイルファースト」、ウェブサイトやアプリケーションをまずモバイル向けに設計・構築することを推奨するものです。</p>
<p>目次は以下のようなものです。</p>
<blockquote>
<p>Part1 Why Mobile First?<br>CHAPTER1 Growth<br>CHAPTER2 Contstraints<br>CHAPTER3 Capabilities<br>Part2 How to go Mobile<br>CHAPTER4 Organization<br>CHAPTER5 Actions<br>CHAPTER6 Inputs<br>CHAPTER7 Layout</p>
</blockquote>
<p>「Part1 Why Mobile First?」では、当時のモバイルデバイスのシェア率の拡大を背景に、「モバイルファースト」とするメリットをさまざまな角度から論じています。</p>
<blockquote>
<p>So it’s not hard to imagine how another bold prediction<br>like “mobile phones will overtake PCs as the most common<br>web access devices worldwide by 2013” will happen much<br>earlier than predicted as well (http://bkaprt.com/mf/10).<br>(p.8を引用)</p>
</blockquote>
<p>上記では、「したがって、「モバイル電話は2013年までに世界中で最も一般的なウェブアクセスデバイスとしてPCを上回るだろう」というような大胆な予測が、予想よりもはるかに早く実現するのは難しくありません」と言った趣旨の内容が記載されており、「2013年までに」こそ日本では実現しましたが、現時点では実際にスマートフォンの普及率がPCと逆転していることもあり感慨深いです。</p>
<p>また、UI設計に関するセクションとしては、CHATPTER5 ~ 7が具体的な内容に踏み込んでいます。</p>
<p>「CHAPTER5 Actions」では、タップ操作を始めとしたタッチ対象オブジェクトのサイズについてや、タッチジェスチャーについて言及されています。また、本記事の前項で紹介したNUIが紹介されていますが、GUIとNUIの過渡期にあることを念頭にインターフェースデザインを考える必要があると記載されてもいます。</p>
<p>「CHAPTER6 Inputs」では、モバイルアプリでの入力タイプについて言及されています。ウェブサイトで使用されてきた、チェックボックスやラジオボタン、パスワード、ドロップダウン、ファイルの選択、送信ボタン、プレーンテキストなどについて、モバイルデバイス特有の意識すべきことなどが記載されています。</p>
<p>「CHAPTER7 Layout」では、モバイルデバイス特有のレイアウトについて言及されています。レスポンシブルの必要性や、シンプルに作ることの重要性については、今も変わらない重要なことであると感じさせられました。</p>
<p>本書では、モバイルデバイスの急速な普及を背景に、「モバイルファースト」の思想について記載されていました。優れたUIについて言及する中で、様々な既存アプリの比較も行っており、モバイルアプリの発展自体が背景にあるようにも考えられる内容で興味深かったです。</p>
<h3 id="5-Microinteractions-2013">5. Microinteractions (2013)</h3><p>https://www.oreilly.com/library/view/microinteractions/9781449342760/</p>
<p>次はこちらのDan Safferによる執筆、O’Reilly Media出版の書籍を紹介します。<br>本書は以下の目次で構成されています。</p>
<blockquote>
<p>Chapter 1, Designing Microinteractions<br>Chapter 2, Triggers<br>Chapter 3, Rules<br>Chapter 4, Feedback<br>Chapter 5, Loops and Modes<br>Chapter 6, Putting It All Together</p>
</blockquote>
<p>本書では、「Microinteractions」を製品の機能的のうち細部のインタラクティブなものを示しています。例えば、パスワードのバリデーションチェック(今入力しているのものが、OKなのか、文字数不足なのかのリアルタイム表示)が挙げられます。</p>
<p>詳細については本書を読んでいただきたいのですが、マイクロインタラクションについて、本書では「Triggers」「Rules」「Feedback」「Loop&amp;Modes」の４つで構成される、として定義し、それぞれの定義や原則について詳細に記載されています。</p>
<p>ユーザーが直感的に操作する上で、明確なフィードバックがあることは重要で、常に意識して設計していきたい要素であると感じました。</p>
<h3 id="6-Material-Design-2014-2018-2021">6. <strong>Material Design (2014&#x2F;2018&#x2F;2021)</strong></h3><p>https://m1.material.io/<br>https://m2.material.io/<br>https://m3.material.io/</p>
<p>モバイルアプリに固有の内容ではありませんが、こちらは2014年にGoogleが提唱したUI&#x2F;UXのデザインである「Material Design」の紹介です。</p>
<p>※Material DesignはM1,M2,M3の3つが存在しますが、2014年に提唱された最初のものがM1となります。</p>
<p>当時提唱されたMaterial Designの目標としては、以下の2つが記載されています。</p>
<blockquote>
<p>Create a visual language that synthesizes classic principles of good design with the innovation and possibility of technology and science.<br>(直訳: 良いデザインの古典的な原則と技術や科学の革新と可能性を統合する視覚言語を作成する。)</p>
</blockquote>
<blockquote>
<p>Develop a single underlying system that allows for a unified experience across platforms and device sizes. Mobile precepts are fundamental, but touch, voice, mouse, and keyboard are all ﬁrst-class input methods.<br>(直訳: プラットフォームやデバイスのサイズを問わず、統一された体験を可能にする単一の基盤となるシステムを開発する。モバイルの原則は基本的であるが、タッチ、音声、マウス、キーボードすべてが第一級の入力方法である。)</p>
</blockquote>
<p>また、原則として以下の3つも掲げており、次が特徴として記載されています。</p>
<ol>
<li><strong>Material is the metaphor</strong><ul>
<li>紙のような物理的性質を持つインターフェースを意識し、ユーザーが直感的に操作しやすいUIを作る基盤となること</li>
</ul>
</li>
<li><strong>Bold, graphic, intentional</strong><ul>
<li>意図的な色の選択、エッジツーエッジの画像、大規模なタイポグラフィ、意図的な空白は、ユーザーをエクスペリエンスに没頭させる大胆でグラフィックなインターフェースを作成すること</li>
</ul>
</li>
<li><strong>Motion provides meaning</strong><ul>
<li>モーションが意味があり適切であり、注意を集中させ、継続性を維持させること</li>
</ul>
</li>
</ol>
<p>これらの思想だけではなく、Material Designに沿ったダイアログ、カードを始めとした多様なコンポーネントを例示していることも大きな特徴です。これらの導入により、特にAndroidではモバイルアプリのUI設計においても一貫性が強調できたのではないでしょうか？</p>
<p>2023年時点ではMaterial Design 3が最新となっていますが、アプリ開発をする上で個人的にも多用させていただいているものとなります。</p>
<h2 id="近年・将来のモバイルアプリUI設計のトレンドについて考察">近年・将来のモバイルアプリUI設計のトレンドについて考察</h2><p>前のセクションでは、モバイルアプリUI設計に関する理論や代表的な書籍を選び、簡単に紹介させていただきました。</p>
<p>このセクションでは、将来のトレンドに焦点を当てて、私の個人的な考えを共有します。</p>
<p>まず、モバイルアプリUI設計の過去の推移を振り返ると、2007年のiPhone登場以降、初期段階では既存のウェブサイトのレイアウトをモバイルに適応・進化させる試みが盛んに行われました。その後、モバイルデバイスのシェアが急速に拡大する中、「モバイルファースト」「レスポンシブデザイン」「NUI（Natural User Interface）」といった理論が発展し、ガイドラインが整備され、設計プロセスが成熟していきました。</p>
<p>これらの理論の中でも、「タッチ入力」によるユーザーインターフェースの進化が中心的な役割を果たしていると考えることができます。</p>
<p>それでは、これからのトレンドはどのようなものが考えられるでしょうか？</p>
<p>1つ目のトレンドとして、デバイスの進化、特に「タッチ入力」を補完または置き換える新しい入力方法の登場が考えられます。例えば、SiriやAlexa、Google Assistantに挙げられるような、音声ユーザーインターフェース（VUI）や、Meta Quest3に搭載されている視線追跡技術などがあげられます。これらの技術がモバイルデバイスで「あたりまえ」になれば、「タッチ入力」が時代を変えたように、モバイルアプリUI設計も大きく変わるかもしれません。</p>
<p>2つ目のトレンドとして、AIを活用したモバイルアプリUI設計の変革が考えられます。ChatGPTをはじめとする近年のAI技術の進化は著しく、実際に以下のような、強化学習を用いた適応型UI、AUI(Adapting User Interface)の研究もされています。これにより、ユーザーの行動を学習し、UIを自動で最適化したり、あるいは個々のユーザーに合わせたUIを提供できる時代が来るかもしれません。</p>
<ul>
<li>Adapting User Interfaces with Model-based Reinforcement Learning</li>
<li>MARLUI: Multi-Agent Reinforcement Learning for Adaptive UIs</li>
</ul>
<h2 id="まとめ">まとめ</h2><p>本記事では、モバイルアプリUI設計に関わる書籍や理論をいくつかピックアップし、簡単に紹介しました。</p>
<p>現在では、これらの設計・理論・技術を基盤として、新たな入力インターフェースや、AIを用いたUIの最適化などが研究・議論されています。</p>
<p>本記事が、モバイルアプリのUI設計についてなにか考えるきっかけになれば幸いです。</p>
<p>次は真野さんの 設計ドキュメント腐る問題、Git管理で運用してみた結果 です。</p>
<h2 id="参考文献">参考文献</h2><ol>
<li>Wonjin Jung. “The Effect of Representational UI Design Quality of Mobile Shopping Applications on Users’ Intention to Shop”. Procedia Computer Science, Volume 121, 2017, Pages 166-169, 2017</li>
<li>Designers. “Know Your User – UX Statistics and Insights (with Infographic)”.  https://www.toptal.com/designers/ux/ux-statistics-insights-infographic</li>
<li>情報通信統計データベース.”通信利用動向調査”. https://www.soumu.go.jp/johotsusintokei/statistics/statistics05.html</li>
<li>Apple. “Human Interface Guidelines”. https://developer.apple.com/design/human-interface-guidelines</li>
<li>Brian Fling.『Mobile Design and Development: Practical concepts and techniques for creating mobile sites and web apps』. O’Reilly Media. 2009</li>
<li>Andre Charland, Brian LeRoux. “Mobile Application Development: Web vs. Native: Web apps are cheaper to develop and deploy than native apps, but can they match the native user experience?”. Queue. Volume 9, Issue 4. 2011</li>
<li>Andre Charland, Brian LeRoux, Nitobi.”Mobile Application Development:Web vs. Native”. Queue.</li>
<li>stack overflow. “Fixed positioning in Mobile Safari” .https://stackoverflow.com/questions/743123/fixed-positioning-in-mobile-safari</li>
<li>ANANDTECH. “BlackBerry Torch 9800 Review: Keeping RIM’s Flame Alive”.https://www.anandtech.com/show/3880/blackberry-torch-9800-review-keeping-rims-flame-alive/7</li>
<li>Jesse James Garrett. “The Elements of User Experience”.http://www.jjg.net/elements/pdf/elements.pdf</li>
<li>A LIST APART.Ethan Marcotte. “Responsive Web Design”. 2010</li>
<li>Daniel Wigdor, Dennis Wixon. 『Brave NUI World: Designing Natural User Interfaces for Touch and Gesture by Daniel Wigdor and Dennis Wixon』. Morgan Kaufmann. 2011.</li>
<li>Luke Wroblewski.『Mobile First』.Ingram. 2011</li>
<li>Dan Saffer.『Microinteractions』. O’Reilly Media. 2013</li>
<li>Google. “Material Design”. https://m1.material.io/</li>
<li>Kashyap Todi,Gilles Bailly, Luis A. Leiva, Antti Oulasvirta. “Adapting User Interfaces with Model-based Reinforcement Learning”. 2021</li>
<li>Thomas Langerak, Sammy Christen, Mert Albaba, Christoph Gebhardt, Otmar Hilliges. “Thomas Langerak, Sammy Christen, Mert Albaba, Christoph Gebhardt, Otmar Hilliges”. arxiv. 2022</li>
</ol>
]]></content>
    <summary type="html">※この記事は、秋のブログ週間2023の２本目の記事となります。2023年の秋、スマートフォンやタブレットは私たちの日常生活に欠かせない存在となっています。これらのデバイスに対するアプリケーション開発の中でも、UI設計の議論については欠かすことができません。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="PWA" scheme="https://future-architect.github.io/tags/PWA/"/>
    <category term="UI/UX" scheme="https://future-architect.github.io/tags/UI-UX/"/>
  </entry>
  <entry>
    <title>【Flutter】Proxyがある社内ネットワーク環境でAndroidエミュレータからインターネットに接続する方法</title>
    <link href="https://future-architect.github.io/articles/20231026a/"/>
    <id>https://future-architect.github.io/articles/20231026a/</id>
    <published>2023-10-25T15:00:00.000Z</published>
    <updated>2023-10-25T15:00:00.000Z</updated>
    <author><name>後藤田千里</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは。思い出をショートムービーとして残すアプリ、PostPixを作っている後藤田です。</p>
<p>PostPixはFlutterで作っています。ぜひ、ダウンロードしてあなたの思い出をショートムービーにしてみてください！</p>
<style>
.linkable_img:hover {opacity:0.5;}
</style>


    <img fetchpriority="high" src="/images/2023/20231026a/b6fc4438-8326-63c9-a373-1143d4d88207.png" alt="" width="1200" height="1067">


<h2 id="Androidエミュレータからプロキシサーバを超えてインターネットに出たい">Androidエミュレータからプロキシサーバを超えてインターネットに出たい</h2><p>プロキシが存在するネットワーク内で、Androidエミュレータからインターネット接続ができず、頭を悩ませたことはありませんか？</p>
<p>私たちは、この課題に対する解決策として、middleproxy（社内のエンジニアが作ってくれたツール）とFlutterのnative_flutter_proxyライブラリを活用しています。</p>
<p>この記事ではそれらについて解説します。</p>
<p>最終的な構成は次のようになります。</p>
<img src="/images/2023/20231026a/env.png" alt="" width="800" height="353" loading="lazy">

<p>なお、開発環境はWindows。プロキシは認証つきで、独自の証明書を利用する前提とします。</p>
<h2 id="1-Android仮想デバイスの作成">1. Android仮想デバイスの作成</h2><p>まずは、Android仮想デバイス（AVD）を作成します。今回は <code>Pixel_4_XL</code> を利用するとします。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">%UserProfile%\AppData\Local\Android\sdk\emulator\emulator -writable-system -avd Pixel_4_XL_API_30</span><br></pre></td></tr></table></figure>

<p><code>%USERPROFILE%/.android</code>に <code>advancedFeatures.ini</code> を作成し、以下の2行を追加します。</p>
<figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="attr">Vulkan</span> = <span class="literal">off</span></span><br><span class="line"><span class="attr">GLDirectMem</span> = <span class="literal">on</span></span><br></pre></td></tr></table></figure>

<h2 id="2-adbのインストール">2. adbのインストール</h2><p>Android Debug Bridge（adb）をインストールします。</p>
<p>必要に応じて、adbにPATHは通しておきます。</p>
<p>以下のように出力されればOKです。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">adb --version</span></span><br><span class="line">Android Debug Bridge version 1.0.41</span><br><span class="line">Version 33.0.3-8952118</span><br><span class="line">Installed as C:\Program Files\platform-tools\adb.exe</span><br></pre></td></tr></table></figure>

<h2 id="3-CA証明書のエミュレータへの転送">3. CA証明書のエミュレータへの転送</h2><p>プロキシが要求する証明書が <code>custom_ca.cer</code> であり、その証明書をDesktop直下に配備したとします。</p>
<p>これをホストPCから、Androidエミュレータ側の、 <code>/sdcard/Download</code> にパスに転送します。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cd</span> %USERPROFILE%\Desktop</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb root</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb disable-verity</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb shell avbctl disable-verification</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb reboot</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">reboot 後に待機</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb root</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb remount</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb push custom_ca.cer /sdcard/Download</span></span><br></pre></td></tr></table></figure>

<h2 id="4-証明書の読み込み">4. 証明書の読み込み</h2><p>エミュレータ上のAndroidを操作します。</p>
<p><code>歯車マーク</code> &gt; <code>Security</code> &gt; <code>Advanced</code> &gt; <code>Encryption &amp; credentials</code> &gt; <code>Install a certificate</code> &gt; <code>CA certificate</code> &gt; <code>Install anyway</code> &gt; 上のハンバーガーメニューから <code>Android SDK build for x86 を選択</code> &gt; <code>Download を開く</code></p>
<p>…すると、先ほど追加した、<code>custom_ca.cer</code> があるので、クリックして読み込みます。</p>
<h2 id="5-ローカルプロキシ起動">5. ローカルプロキシ起動</h2><p>middleproxyから <code>middleproxy_0.0.3_Windows_i386.tar.gz</code> をダウンロード &amp; 解凍（ご自身の環境に合わせてダウンロードしてください）します。</p>
<p>次のようにプロキシ情報を環境変数に設定して、起動します。</p>
<figure class="highlight sh"><figcaption><span>起動イメージ</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="built_in">set</span> http_proxy=http://&#123;username&#125;:&#123;password&#125;@proxy.example.com:8000</span><br><span class="line"><span class="built_in">set</span> http_proxy_username=&#123;username&#125;</span><br><span class="line"><span class="built_in">set</span> http_proxy_password=&#123;password&#125;</span><br><span class="line">middleproxy.exe <span class="comment">#起動してもログは出ません。</span></span><br></pre></td></tr></table></figure>

<h2 id="6-エミュレータ上のプロキシ設定">6. エミュレータ上のプロキシ設定</h2><p><code>WiFiマークを長押し</code> &gt; <code>AndroidWifi の歯車マーク</code> &gt; <code>右上の鉛筆マーク</code> &gt; <code>Advanced options</code> &gt; <code>Proxy</code> &gt; <code>Manual</code></p>
<ul>
<li>Proxy hostname <code>10.0.2.2</code></li>
<li>Proxy port: <code>9000</code></li>
</ul>
<p>を入力して保存します。</p>
<p>補足ですが、 <code>10.0.2.2</code> はエミュレータ上のAndroidからみた、ホスト側のIPです。</p>
<h2 id="7-アプリ側の設定をする">7. アプリ側の設定をする</h2><p>設定ファイルに以下のネットワーク・セキュリティ設定を加えます。</p>
<figure class="highlight xml"><figcaption><span>AndroidManifest.xml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">application</span></span></span><br><span class="line"><span class="tag">   <span class="attr">android:networkSecurityConfig</span>=<span class="string">&quot;@xml/network_security_config&quot;</span></span></span><br></pre></td></tr></table></figure>

<p><code>res/xml/network_security_config.xml</code> を作成し、次を追加します。</p>
<figure class="highlight xml"><figcaption><span>res/xml/network_security_config.xml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;utf-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">network-security-config</span> <span class="attr">xmlns:tools</span>=<span class="string">&quot;http://schemas.android.com/tools&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">debug-overrides</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">trust-anchors</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">certificates</span> <span class="attr">src</span>=<span class="string">&quot;@raw/custom_ca&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">trust-anchors</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">debug-overrides</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">network-security-config</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><code>res/raw</code> に <code>custom_ca.cer</code> を配備して参照できるようにします。</p>
<h2 id="8-native-flutter-proxy">8. native_flutter_proxy</h2><p>さて、やっとDartのコードです。<code>native_flutter_proxy</code> ライブラリの力を借りて、エミュレータ上のAndroidアプリからもインターネット接続を可能にします。</p>
<figure class="highlight dart"><figcaption><span>main.dart</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// Flutterのmain.dartや適当な初期化箇所に以下を追加</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;package:native_flutter_proxy/custom_proxy.dart&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;package:native_flutter_proxy/native_proxy_reader.dart&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> main() <span class="keyword">async</span> &#123;</span><br><span class="line">  WidgetsFlutterBinding.ensureInitialized();</span><br><span class="line"></span><br><span class="line">  <span class="built_in">bool</span> enabled = <span class="keyword">false</span>;</span><br><span class="line">  <span class="built_in">String?</span> host;</span><br><span class="line">  <span class="built_in">int?</span> port;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    ProxySetting settings = <span class="keyword">await</span> NativeProxyReader.proxySetting;</span><br><span class="line">    enabled = settings.enabled;</span><br><span class="line">    host = settings.host;</span><br><span class="line">    port = settings.port;</span><br><span class="line">  &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">    <span class="built_in">print</span>(e);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (enabled &amp;&amp; host != <span class="keyword">null</span>) &#123;</span><br><span class="line">    <span class="keyword">final</span> proxy = CustomProxy(ipAddress: host, port: port);</span><br><span class="line">    proxy.enable();</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;proxy enabled&quot;</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  runApp(MyApp());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><code>native_flutter_proxy</code> ですが、エミュレータの端末に設定されたシステムのプロキシ設定を読み込み、インターネット接続を行えるようにしてくれます。</p>
<h3 id="※それでもインターネット接続できない場合">※それでもインターネット接続できない場合</h3><ol>
<li>Androidエミュレータ側のプロキシ設定を外してみる（direct）</li>
<li>localhost:9000 にmiddleproxyが起動しているか確認する（例： ホスト側のプロキシをproxy.example.com:8000 から、 localhost:9000 に変えてみて、ホスト側のブラウザが起動するか、もしくはcurlが通るか、確認してみる）</li>
</ol>
<h2 id="（任意）AndroidエミュレータへのChromeインストール">（任意）AndroidエミュレータへのChromeインストール</h2><p>ここは任意ですが、エミュレータAndroid上にChromeをインストールしておくと、疎通などで便利なことがあるので合わせて設定しておくことをオススメします。</p>
<p>こちらからapkをダウンロードし、エミュレータ画面にdrag &amp; dropします。</p>
<h2 id="おわりに">おわりに</h2><p>本記事では、Flutterにおけるプロキシが存在するネットワーク環境での、Androidエミュレータのインターネット接続方法について紹介しました。</p>
<p>この手順を参考に、効果的にエミュレータを利用し、みなさんの開発に役立てれば幸いです。</p>
<p>最後にまた宣伝させてください！！  </p>
<p>PostPixであなたの思い出をショートムービーという新しい形で残してみませんか？？<br>PostPixはFlutterで作成しています。ぜひダウンロードのほどお願いします！</p>

    <img src="/images/2023/20231026a/PostPix2.png" alt="" width="1834" height="942" loading="lazy">

]]></content>
    <summary type="html">プロキシが存在するネットワーク内で、Androidエミュレータからインターネット接続ができず、頭を悩ませたことはありませんか？</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Android" scheme="https://future-architect.github.io/tags/Android/"/>
    <category term="Flutter" scheme="https://future-architect.github.io/tags/Flutter/"/>
    <category term="エミュレータ" scheme="https://future-architect.github.io/tags/%E3%82%A8%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF/"/>
    <category term="ネットワーク" scheme="https://future-architect.github.io/tags/Network/"/>
    <category term="プロキシ" scheme="https://future-architect.github.io/tags/%E3%83%97%E3%83%AD%E3%82%AD%E3%82%B7/"/>
  </entry>
  <entry>
    <title>SwiftUIのカスタムアラートダイアログについて考える</title>
    <link href="https://future-architect.github.io/articles/20230908a/"/>
    <id>https://future-architect.github.io/articles/20230908a/</id>
    <published>2023-09-07T15:00:00.000Z</published>
    <updated>2023-09-07T15:00:00.000Z</updated>
    <author><name>清水雄一郎</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは。HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>所属の清水です。</p>
<p>本記事は、夏の自由研究ブログ連載2023の5本目です。</p>
<p>SwiftUIにおけるアラートダイアログを自作で実装する場合、どんな方法があるか考えてみました。</p>
<p>Swiftを用いたiOSアプリ開発で、アラートを自作したいと考えたことはありませんか？ Webアプリでもモバイルアプリでも、ユーザに問題を報告したり動作による影響を警告したりする上で、アラートは重要です。</p>
<p>SwiftUIでも<code>alert</code>が標準で用意されています。ただし、フォントサイズやダイアログのサイズが変更できないため、iPadで利用する場合画面に対してかなり小さく扱いづらいです。</p>
<p>SwiftUI標準の<code>alert</code>をシミュレータ iPad Pro (12.9-inch) で表示した際の図</p>
<img fetchpriority="high" src="/images/2023/20230908a/68747470733a2f2.png" alt="" width="1200" height="1643">

<p>画面に対してかなり小さい・・・。</p>
<p>そこで、「自由にカスタマイズできるアラートダイアログを自作する場合、どのような実装方法があるか」を自由研究の題材とすることにしました。</p>
<p>今回試してみた3種類のカスタムアラートを、実装方法と共に紹介したいと思います。<br>※もっと良い実装方法をご存知でしたら、教えていただきたいです！</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>本記事では触れていませんが、カスタムアラートダイアログに関するライブラリも公開されています。合わせて確認頂けると良いと思います（例: CustomAlert）。</p>
</div></div>

<h2 id="検証内容">検証内容</h2><p>アラートは、通常の画面より手前（通常画面に重なる形）に表示します。<br>今回は、重ねて表示するためのコンポーネントの中から、以下の3種類の実装方法で比較していきたいと思います。</p>
<ol>
<li><code>fullScreenCover</code>を利用した方法</li>
<li><code>overlay</code>を利用した方法</li>
<li><code>ZStack</code>を利用した方法</li>
</ol>
<h3 id="事前準備">事前準備</h3><p>画面に重ねて表示するためのダイアログをViewとして用意しています。SwiftUI標準の<code>alert</code>に見た目を寄せたアラートViewを、iPad Pro (12.9-inch) 向けにサイズ調整したViewがこちらです。このViewをいくつかの方法で重ねていきます。</p>
<p>※実装は、記事後半に記載しています。</p>
<img src="/images/2023/20230908a/68747470733a2f2_2.png" alt="" width="1140" height="1420" loading="lazy">

<h3 id="1-fullScreenCoverを利用した方法">1. <code>fullScreenCover</code>を利用した方法</h3><p>1つ目は、<code>fullScreenCover</code>を利用した方法です。</p>
<p>引数に渡したフラグがtrueの時、全画面を覆うViewをモーダル表示します。</p>
<p>他の方法と比較</p>
<ul>
<li>メリット<ul>
<li>画面全体にモーダル表示するため、アラート表示中に他の動作ができないように制限しやすい。</li>
</ul>
</li>
<li>デメリット<ul>
<li><code>fullScreenCover</code> は表示時に画面下から上まで覆うアニメーションを伴って表示するため、アニメーションのカスタマイズが難しい（調べた限りほぼできない）。<br>アニメーションを非活性にすることはできる。<sup id="fnref:2">2</sup></li>
</ul>
</li>
</ul>
<img src="/images/2023/20230908a/68747470733a2f2.gif" alt="" width="512" height="683" loading="lazy">

<h3 id="2-overlayを利用した方法">2. <code>overlay</code>を利用した方法</h3><p>2つ目は、<code>overlay</code>を利用した方法です。<br>名前の通り、親Viewより手前に指定したViewを重ねて表示します。アラートの使用用途を考えるとちょうど良さそうです。</p>
<p>他の方法と比較</p>
<ul>
<li>メリット<ul>
<li><code>overlay</code>自体は重ねて表示するだけなので、アニメーションなどをカスタマイズしやすい。</li>
</ul>
</li>
<li>デメリット<ul>
<li>あくまでも元となるViewに重ねるため、重ねて表示する範囲は呼び出すViewに依存する。<br>例えば、<code>VStack</code>に紐づけると<code>VStack</code>の範囲が重ねるViewの表示範囲となり、表示範囲外は操作できてしまう（下のgif参照）</li>
</ul>
</li>
</ul>
<img src="/images/2023/20230908a/68747470733a2f2_2.gif" alt="" width="512" height="683" loading="lazy">

<h3 id="3-ZStackを利用した方法">3. <code>ZStack</code>を利用した方法</h3><p>3つ目は、<code>ZStack</code>を利用した方法です。<br><code>fullScreenCover</code>や<code>overlay</code>はViewのInstance Methodを利用していたのですが、こちらは内包するViewの表示順を制御するものなので、毛色が異なります。</p>
<p>他の方法と比較</p>
<ul>
<li>メリット<ul>
<li>アプリ大元のViewに重ねることで、画面全体を覆って表示制御できる。</li>
</ul>
</li>
<li>デメリット<ul>
<li>ダイアログ表示を制御するフラグやダイアログに表示する内容を<code>App</code>階層まで伝える必要があるため、単一のView内で状態管理が完結しない。</li>
</ul>
</li>
</ul>
<img src="/images/2023/20230908a/68747470733a2f2_3.gif" alt="" width="512" height="683" loading="lazy">

<h3 id="コード実装例">コード実装例</h3><p>今回利用したソースコードは、こちらです。</p>
<h4 id="App">App</h4><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">IsShowAlert</span>: <span class="title class_ inherited__">EnvironmentKey</span> &#123;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">let</span> defaultValue: <span class="type">Binding</span>&lt;<span class="type">Bool</span>&gt; <span class="operator">=</span> .constant(<span class="literal">false</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">extension</span> <span class="title class_">EnvironmentValues</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> isShowAlert: <span class="type">Binding</span>&lt;<span class="type">Bool</span>&gt; &#123;</span><br><span class="line">        <span class="keyword">get</span> &#123; <span class="keyword">self</span>[<span class="type">IsShowAlert</span>.<span class="keyword">self</span>] &#125;</span><br><span class="line">        <span class="keyword">set</span> &#123; <span class="keyword">self</span>[<span class="type">IsShowAlert</span>.<span class="keyword">self</span>] <span class="operator">=</span> newValue &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@main</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">SampleApp</span>: <span class="title class_ inherited__">App</span> &#123;</span><br><span class="line">    <span class="meta">@State</span> <span class="keyword">private</span> <span class="keyword">var</span> isShowAlert: <span class="type">Bool</span> <span class="operator">=</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">Scene</span> &#123;</span><br><span class="line">        <span class="type">WindowGroup</span> &#123;</span><br><span class="line">            <span class="type">ZStack</span> &#123;</span><br><span class="line">                <span class="type">ContentView</span>()</span><br><span class="line">                    .environment(\.isShowAlert, <span class="variable">$isShowAlert</span>)</span><br><span class="line">                <span class="comment">/// 3. `ZStack`を利用した実装方法</span></span><br><span class="line">                <span class="keyword">if</span> isShowAlert &#123;</span><br><span class="line">                    <span class="type">CustomAlertView</span>(alertTitle: <span class="type">Text</span>(<span class="string">&quot;タイトル&quot;</span>)) &#123;</span><br><span class="line">                        <span class="type">Button</span>(<span class="string">&quot;OK&quot;</span>) &#123;</span><br><span class="line">                            <span class="variable">$isShowAlert</span>.wrappedValue.toggle()</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125; message: &#123;</span><br><span class="line">                        <span class="type">Text</span>(<span class="string">&quot;メッセージ&quot;</span>)</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="ContentView">ContentView</h4><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">ContentView</span>: <span class="title class_ inherited__">View</span> &#123;</span><br><span class="line">    <span class="comment">/// 標準アラートの表示を管理するフラグ</span></span><br><span class="line">    <span class="meta">@State</span> <span class="keyword">var</span> isShowDefaultAlert: <span class="type">Bool</span> <span class="operator">=</span> <span class="literal">false</span></span><br><span class="line">    <span class="comment">/// `fullScreenCover`を利用したカスタムアラートの表示を管理するフラグ</span></span><br><span class="line">    <span class="meta">@State</span> <span class="keyword">var</span> isShowFullScreenCoverAlert: <span class="type">Bool</span> <span class="operator">=</span> <span class="literal">false</span></span><br><span class="line">    <span class="comment">/// `overlay`を利用したカスタムアラートの表示を管理するフラグ</span></span><br><span class="line">    <span class="meta">@State</span> <span class="keyword">var</span> isShowOverlayAlert: <span class="type">Bool</span> <span class="operator">=</span> <span class="literal">false</span></span><br><span class="line">    <span class="comment">/// `ZStack`を利用したカスタムアラートの表示を管理するフラグ</span></span><br><span class="line">    <span class="meta">@Environment</span>(\.isShowAlert) <span class="keyword">var</span> isShowAlert</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">ZStack</span> &#123;</span><br><span class="line">            <span class="type">VStack</span>(spacing: <span class="number">50</span>) &#123;</span><br><span class="line">                <span class="type">Button</span>(</span><br><span class="line">                    action: &#123;</span><br><span class="line">                        isShowDefaultAlert <span class="operator">=</span> <span class="literal">true</span></span><br><span class="line">                    &#125;,</span><br><span class="line">                    label: &#123;</span><br><span class="line">                        <span class="type">Text</span>(<span class="string">&quot;標準アラートを表示する&quot;</span>)</span><br><span class="line">                    &#125;</span><br><span class="line">                )</span><br><span class="line">                <span class="type">Button</span>(</span><br><span class="line">                    action: &#123;</span><br><span class="line">                        isShowFullScreenCoverAlert <span class="operator">=</span> <span class="literal">true</span></span><br><span class="line">                    &#125;,</span><br><span class="line">                    label: &#123;</span><br><span class="line">                        <span class="type">Text</span>(<span class="string">&quot;`fullScreenCover`のアラートを表示する&quot;</span>)</span><br><span class="line">                    &#125;</span><br><span class="line">                )</span><br><span class="line">                <span class="type">Button</span>(</span><br><span class="line">                    action: &#123;</span><br><span class="line">                        isShowOverlayAlert <span class="operator">=</span> <span class="literal">true</span></span><br><span class="line">                    &#125;,</span><br><span class="line">                    label: &#123;</span><br><span class="line">                        <span class="type">Text</span>(<span class="string">&quot;`overlay`のアラートを表示する&quot;</span>)</span><br><span class="line">                    &#125;</span><br><span class="line">                )</span><br><span class="line">                <span class="type">Button</span>(</span><br><span class="line">                    action: &#123;</span><br><span class="line">                        isShowAlert.wrappedValue <span class="operator">=</span> <span class="literal">true</span></span><br><span class="line">                    &#125;,</span><br><span class="line">                    label: &#123;</span><br><span class="line">                        <span class="type">Text</span>(<span class="string">&quot;`ZStack`のアラートを表示する&quot;</span>)</span><br><span class="line">                    &#125;</span><br><span class="line">                )</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        .alert(<span class="type">Text</span>(<span class="string">&quot;タイトル&quot;</span>), isPresented: <span class="variable">$isShowDefaultAlert</span>) &#123;</span><br><span class="line">            <span class="type">Button</span>(<span class="string">&quot;OK&quot;</span>) &#123;</span><br><span class="line">                isShowDefaultAlert.toggle()</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; message: &#123;</span><br><span class="line">            <span class="type">Text</span>(<span class="string">&quot;メッセージ&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        .alertFullScreenCover(<span class="type">Text</span>(<span class="string">&quot;タイトル&quot;</span>), isPresented: <span class="variable">$isShowFullScreenCoverAlert</span>) &#123;</span><br><span class="line">            <span class="type">Button</span>(<span class="string">&quot;OK&quot;</span>) &#123;</span><br><span class="line">                isShowFullScreenCoverAlert.toggle()</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; message: &#123;</span><br><span class="line">            <span class="type">Text</span>(<span class="string">&quot;メッセージ&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        .alertOverlay(<span class="type">Text</span>(<span class="string">&quot;タイトル&quot;</span>), isPresented: <span class="variable">$isShowOverlayAlert</span>) &#123;</span><br><span class="line">            <span class="type">Button</span>(<span class="string">&quot;OK&quot;</span>) &#123;</span><br><span class="line">                isShowOverlayAlert.toggle()</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; message: &#123;</span><br><span class="line">            <span class="type">Text</span>(<span class="string">&quot;メッセージ&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="Extensions">Extensions</h4><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">extension</span> <span class="title class_">View</span> &#123;</span><br><span class="line">    <span class="comment">/// 1. `fullScreenCover`を利用した実装方法</span></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">alertFullScreenCover</span>&lt;<span class="type">A</span>, <span class="type">M</span>&gt;(</span><br><span class="line">        <span class="keyword">_</span> <span class="params">title</span>: <span class="type">Text</span>,</span><br><span class="line">        <span class="params">isPresented</span>: <span class="type">Binding</span>&lt;<span class="type">Bool</span>&gt;,</span><br><span class="line">        <span class="meta">@ViewBuilder</span> <span class="params">actions</span>: <span class="keyword">@escaping</span> () -&gt; <span class="type">A</span>,</span><br><span class="line">        <span class="meta">@ViewBuilder</span> <span class="params">message</span>: <span class="keyword">@escaping</span> () -&gt; <span class="type">M</span></span><br><span class="line">    ) -&gt; <span class="keyword">some</span> <span class="type">View</span> <span class="keyword">where</span> <span class="type">A</span> : <span class="type">View</span>, <span class="type">M</span> : <span class="type">View</span> &#123;</span><br><span class="line">        fullScreenCover(isPresented: isPresented) &#123;</span><br><span class="line">            <span class="type">CustomAlertView</span>(alertTitle: title, actions: actions, message: message)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/// 2. `overlay`を利用した実装方法</span></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">alertOverlay</span>&lt;<span class="type">A</span>, <span class="type">M</span>&gt;(</span><br><span class="line">        <span class="keyword">_</span> <span class="params">title</span>: <span class="type">Text</span>,</span><br><span class="line">        <span class="params">isPresented</span>: <span class="type">Binding</span>&lt;<span class="type">Bool</span>&gt;,</span><br><span class="line">        <span class="meta">@ViewBuilder</span> <span class="params">actions</span>: <span class="keyword">@escaping</span> () -&gt; <span class="type">A</span>,</span><br><span class="line">        <span class="meta">@ViewBuilder</span> <span class="params">message</span>: <span class="keyword">@escaping</span> () -&gt; <span class="type">M</span></span><br><span class="line">    ) -&gt; <span class="keyword">some</span> <span class="type">View</span> <span class="keyword">where</span> <span class="type">A</span> : <span class="type">View</span>, <span class="type">M</span> : <span class="type">View</span> &#123;</span><br><span class="line">        overlay &#123;</span><br><span class="line">            <span class="keyword">if</span> isPresented.wrappedValue &#123;</span><br><span class="line">                <span class="type">CustomAlertView</span>(alertTitle: title, actions: actions, message: message)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="CustomAlertView">CustomAlertView</h4><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> SwiftUI</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">CustomAlertView</span>&lt;<span class="type">A</span>, <span class="type">M</span>&gt;: <span class="title class_ inherited__">View</span> <span class="keyword">where</span> <span class="title class_ inherited__">A</span> : <span class="title class_ inherited__">View</span>, <span class="title class_ inherited__">M</span> : <span class="title class_ inherited__">View</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> alertTitle: <span class="type">Text</span></span><br><span class="line">    <span class="meta">@ViewBuilder</span> <span class="keyword">var</span> actions: () -&gt; <span class="type">A</span></span><br><span class="line">    <span class="meta">@ViewBuilder</span> <span class="keyword">var</span> message: () -&gt; <span class="type">M</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">ZStack</span> &#123;</span><br><span class="line">            <span class="comment">// 背景部分</span></span><br><span class="line">            <span class="type">Color</span>.black</span><br><span class="line">                .opacity(<span class="number">0.2</span>)</span><br><span class="line">                .edgesIgnoringSafeArea(.all)</span><br><span class="line">            <span class="comment">// ダイアログ部分</span></span><br><span class="line">            <span class="type">VStack</span> &#123;</span><br><span class="line">                alertTitle</span><br><span class="line">                    .font(.largeTitle)</span><br><span class="line">                    .bold()</span><br><span class="line">                    .padding(.top, <span class="number">20</span>)</span><br><span class="line">                    .padding(.bottom, <span class="number">10</span>)</span><br><span class="line">                message()</span><br><span class="line">                    .font(.title)</span><br><span class="line">                <span class="type">Divider</span>()</span><br><span class="line">                    .padding(.top, <span class="number">20</span>)</span><br><span class="line">                actions()</span><br><span class="line">                    .font(.title)</span><br><span class="line">                    .padding(.vertical, <span class="number">20</span>)</span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line">            .frame(width: <span class="type">UIScreen</span>.main.bounds.width <span class="operator">/</span> <span class="number">2</span>)</span><br><span class="line">            .background(.white)</span><br><span class="line">            .cornerRadius(<span class="number">15</span>)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">CustomAlert_Previews</span>: <span class="title class_ inherited__">PreviewProvider</span> &#123;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">var</span> previews: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">CustomAlertView</span>(alertTitle: <span class="type">Text</span>(<span class="string">&quot;タイトル&quot;</span>), actions: &#123;</span><br><span class="line">            <span class="type">Button</span>(<span class="string">&quot;OK&quot;</span>) &#123;&#125;</span><br><span class="line">        &#125;, message: &#123;</span><br><span class="line">            <span class="type">Text</span>(<span class="string">&quot;メッセージ&quot;</span>)</span><br><span class="line">        &#125;)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="さいごに">さいごに</h2><p>本記事では、自由研究としてアラートダイアログを自作する際の実装方法に関して、3種類を比較してみました。</p>
<p>アラートダイアログは性質上、表示中他の操作ができないことが求められると思います。今回試した中では、3つ目の<code>ZStack</code>を利用した方法がアプリ大元のViewに重ねることでアラートを一番手前に表示する要件を満たしやすいと感じました。</p>
<p>また、今回試して分かったメリット・デメリットを以下の表にまとめました。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">試した実装方法</th>
<th align="left">メリット</th>
<th align="left">デメリット</th>
</tr>
</thead>
<tbody><tr>
<td align="left">1. <code>fullScreenCover</code>を利用した方法</td>
<td align="left">画面全体にモーダル表示するため、アラート表示中に他の動作ができないように制限しやすい。</td>
<td align="left"><code>fullScreenCover</code> は表示時に画面下から上まで覆うアニメーションを伴って表示するため、アニメーションのカスタマイズが難しい（調べた限りほぼできない）。</td>
</tr>
<tr>
<td align="left">2. <code>overlay</code>を利用した方法</td>
<td align="left"><code>overlay</code>自体は重ねて表示するだけなので、アニメーションなどをカスタマイズしやすい。</td>
<td align="left">あくまでも元となるViewに重ねるため、重ねて表示する範囲は呼び出すViewに依存する。</td>
</tr>
<tr>
<td align="left">3. <code>ZStack</code>を利用した方法</td>
<td align="left">アプリ大元のViewに重ねることで、画面全体を覆って表示制御できる。</td>
<td align="left">ダイアログ表示を制御するフラグやダイアログに表示する内容を<code>App</code>階層まで伝える必要があるため、単一のView内で状態管理が完結しない。</td>
</tr>
</tbody></table></div>
<p>何かしらの参考になれば幸いです。</p>
<h2 id="参考">参考</h2><p>https://developer.apple.com/documentation/SwiftUI/View/alert(_:isPresented:actions:)-1bkka</p>
<p>https://developer.apple.com/documentation/swiftui/view/fullscreencover(ispresented:ondismiss:content:)</p>
<p>https://developer.apple.com/documentation/swiftui/view/overlay(alignment:content:)</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">SwiftUI: fullScreenCover with no animation?</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">SwiftUIにおけるアラートダイアログを自作で実装する場合、どんな方法があるか考えてみました。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="SwiftUI" scheme="https://future-architect.github.io/tags/SwiftUI/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
  </entry>
  <entry>
    <title>夏の自由研究：CreateMLを使ってカブトムシを分類するミニアプリを作ってみる。</title>
    <link href="https://future-architect.github.io/articles/20230831a/"/>
    <id>https://future-architect.github.io/articles/20230831a/</id>
    <published>2023-08-30T15:00:00.000Z</published>
    <updated>2023-08-30T15:00:00.000Z</updated>
    <author><name>橋本竜我</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは！ HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>所属の橋本です。</p>
<p>本記事は、夏の自由研究ブログ連載2023の2本目です。</p>
<p>夏の風物詩であるカブトムシやクワガタを識別するミニアプリを作りました。</p>
<p>夏の自由研究ブログの連載があると聞いて、せっかくなら夏っぽい題材にしたいなと、私が小さい時にたくさん捕まえていたカブトムシを題材にしようと考えました。</p>
<p>「Swiftと機械学習あたりを絡めて何かできないかな？」と考えたところ、CreateMLという様々な機械学習モデルをGUIで作ることができると知ったため、カブトムシやクワガタの画像を学習させたモデルを使ったミニアプリを作ることにしました。</p>
<p>今回は、作成したミニアプリをもとに、CreateMLの使い方について紹介したいと思います。<br><img fetchpriority="high" src="/images/2023/20230831a/image.png" alt="" width="718" height="247"></p>
<p><strong>作成したカブトムシ・クワガタを分類するミニアプリ</strong><br><img src="/images/2023/20230831a/beatle_app.gif" alt="" width="177" height="384" loading="lazy"></p>
<h2 id="CreateML、CoreMLとは">CreateML、CoreMLとは</h2><p>CreateMLとは、Appleが提供する機械学習のフレームワークの1つで、機械学習モデルを作成するツールです。また、このCreateMLで作成したモデルを実際にiOSアプリなどで使用ために使われるフレームワークとしてCoreMLがあります。簡単に説明すると、</p>
<ul>
<li>CreateMLが機械学習モデル（画像認識、オブジェクト認識、音声認識、文字認識など）を作成するもの</li>
<li>CoreMLが作成したモデルを様々な実際にアプリに組み込むもの</li>
</ul>
<p>というイメージで問題ないと思います。</p>
<p>以下のApple公式の機械学習に関するページにて、それぞれの説明が記載されています。</p>
<p>https://developer.apple.com/jp/machine-learning/</p>
<p>CreateMLの説明文の引用です。</p>
<blockquote>
<p>Create MLアプリでは、コードを書かずにMac上でCore MLモデルをすばやく構築およびトレーニングすることができます。使いやすいアプリのインターフェイスとトレーニング向けに利用できるモデルにより、プロセスがこれまでになく簡単になり、トレーニングデータさえ用意すれば開始できます。モデルのトレーニングと正確性を可視化する手助けとして、スナップショットやプレビューといった機能でトレーニングプロセスをコントロールすることもできます。Create MLフレームワークおよびCreate ML Componentsを使用すると、モデル作成をより細部まで詳細にコントロールできます。</p>
</blockquote>
<p>CoreMLの説明文の引用です。</p>
<blockquote>
<p>Core MLなら、アプリに機械学習モデルを容易に組み込むことができる上、Appleデバイス上で目を見張るほど速いパフォーマンスを実現します。Core MLのAPIを使うと、構築済みの機械学習機能をアプリに追加することができます。またはCreate MLを使って、カスタムCore MLモデルをMac上で直接トレーニングすることも可能です。他のトレーニングライブラリからのモデルをCore ML Toolsを使って変換したり、すぐに使えるCore MLモデルをダウンロードしたりすることもできます。Xcodeで直接、モデルをプレビューしてそのパフォーマンスを把握することも簡単です。</p>
</blockquote>
<h2 id="CreateMLを使ってカブトムシ・クワガタを識別するモデルの作成からアプリへ実装まで">CreateMLを使ってカブトムシ・クワガタを識別するモデルの作成からアプリへ実装まで</h2><p>まず、CreateMLを使って、カブトムシ・クワガタを識別する機械学習モデルを作成していきます。<br>こちらのページに学習済みのCoreMLが組み込まれたサンプルコードがあるため、こちらの中の<code>MobileNetV2</code>（画像識別のモデル）を使用してミニアプリを作成していきます。</p>
<p>https://developer.apple.com/jp/machine-learning/models/</p>
<p>環境は、次のとおりです。</p>
<h3 id="実装環境">実装環境</h3><ul>
<li>macOS: Ventura 13.4.1(c)</li>
<li>Xcode: Version 14.3.1 (14E300c)</li>
</ul>
<h4 id="学習データ、テストデータを用意する">学習データ、テストデータを用意する</h4><p>4種類のカブトムシ・クワガタの画像を用意します。<br>学習データが100枚、テストデータが20枚それぞれ用意し、以下のフォルダ階層のように準備します。</p>
<h5 id="フォルダ階層">フォルダ階層</h5><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">├trainingData</span><br><span class="line">｜├beatle <span class="comment">#カブトムシ</span></span><br><span class="line">｜├SawToothStagBeatle <span class="comment">#ノコギリクワガタ</span></span><br><span class="line">｜├GiantBeatle <span class="comment">#オオクワガタ</span></span><br><span class="line">｜└MiyamaStagBeatle <span class="comment">#ミヤマクワガタ</span></span><br><span class="line">├testData</span><br><span class="line">　├beatle <span class="comment">#カブトムシ</span></span><br><span class="line">　├SawToothStagBeatle <span class="comment">#ノコギリクワガタ</span></span><br><span class="line">　├GiantStagBeatle <span class="comment">#オオクワガタ</span></span><br><span class="line">　└MiyamaStagBeatle <span class="comment">#ミヤマクワガタ</span></span><br></pre></td></tr></table></figure>

<p>（飼育経験のあるこの四種類を分類することにしてみました）。</p>
<h3 id="識別モデルを作成">識別モデルを作成</h3><p>CreateMLを<code>Xcode</code> &gt; <code>Open Developer Tool</code> &gt; <code>CreatML</code>をクリックし、ウィンドウを開きます。</p>
<p>先程用意したtrainingDataフォルダを以下の”＋”部分に、ドラック＆ドロップで追加します。</p>
<img src="/images/2023/20230831a/image_2.png" alt="" width="1103" height="333" loading="lazy">

<p>追加されると、次のようになります。</p>
<img src="/images/2023/20230831a/image_3.png" alt="" width="1108" height="345" loading="lazy">

<p>左上の<code>Train</code>を押下すると、渡したデータをもとに学習が始まります。</p>
<p>数秒で学習が完了し、以下のように学習時の様子が<code>Trainingタブ</code>で確認できます。</p>
<img src="/images/2023/20230831a/image_4.png" alt="" width="868" height="362" loading="lazy">

<p>これで機械学習モデルの作成自体は完了です。</p>
<p>作成したモデルが未知のデータをどれほどの精度で分類できるか評価する方法として、学習時に使用していないテストデータを渡すことができます。CreateML上での実施方法は、Evaluationタブでtraining時と同様に、Testing dataにテスト用のデータをドラック＆ドロップで追加します。</p>
<img src="/images/2023/20230831a/image_5.png" alt="" width="1114" height="398" loading="lazy">

<p>今回の四種類のカブトムシ・クワガタの分類精度は、ノコギリクワガタが50％をわずかに下回っているが、4分類であることを考えると何もパラメータチューニングをしていない割に、オオクワガタ(giantStagBeatle)は65％とまずまずの精度となっていました。</p>
<h4 id="画像中のMetrics">画像中のMetrics</h4><div class="scroll"><table>
<thead>
<tr>
<th align="left">Class</th>
<th align="right">Count</th>
<th align="right">Correct</th>
<th align="right">FP</th>
<th align="right">FN</th>
<th align="right">Precision</th>
<th align="right">Recall</th>
<th align="right">F1 Score</th>
</tr>
</thead>
<tbody><tr>
<td align="left">SawTooth<br>StagBeatle</td>
<td align="right">20</td>
<td align="right">9</td>
<td align="right">10</td>
<td align="right">11</td>
<td align="right">47%</td>
<td align="right">45%</td>
<td align="right">0.46</td>
</tr>
<tr>
<td align="left">Miyama<br>StagBeatle</td>
<td align="right">20</td>
<td align="right">10</td>
<td align="right">6</td>
<td align="right">10</td>
<td align="right">62%</td>
<td align="right">50%</td>
<td align="right">0.56</td>
</tr>
<tr>
<td align="left">Giant<br>StagBeatle</td>
<td align="right">20</td>
<td align="right">15</td>
<td align="right">8</td>
<td align="right">5</td>
<td align="right">65%</td>
<td align="right">75%</td>
<td align="right">0.7</td>
</tr>
<tr>
<td align="left">beatle</td>
<td align="right">20</td>
<td align="right">9</td>
<td align="right">7</td>
<td align="right">7</td>
<td align="right">59%</td>
<td align="right">65%</td>
<td align="right">0.62</td>
</tr>
</tbody></table></div>
<img src="/images/2023/20230831a/image_6.png" alt="" width="1153" height="480" loading="lazy">

<p>CreateMLで今回作ったモデルをCoreML　Model形式（<code>.mlmodel</code>）として取得します。</p>
<p>Outputタグに移動し、右上のGetから、<code>beatleAndStagBeatlesClassifier.mlmodel</code>を任意の場所に保存できます。</p>
<img src="/images/2023/20230831a/image_7.png" alt="" width="1153" height="553" loading="lazy">

<h3 id="アプリへ実装する">アプリへ実装する</h3><p>こちらのサイト上部のdownloadからサンプルコード（<code>Vision+Core-ML.xcodeproj</code>）を落としてきます。</p>
<p>https://developer.apple.com/documentation/vision/classifying_images_with_vision_and_core_ml</p>
<p>サンプルコードの<code>Vision+Core-ML.xcodeproj</code>プロジェクトを開き、識別時に使用している分類器を<code>MobileNet.mlmodel</code>から<code>beatleAndStagBeatlesClassifier.mlmodel</code>に修正します。</p>
<p>具体的には、<code>ImagePredictor.swift</code>の中の　<code>MobileNet.mlmodel</code>から<code>beatleAndStagBeatlesClassifier.mlmodel</code>に書き換える対応のみを行います。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ImagePredictor</span> &#123;</span><br><span class="line">    <span class="comment">/// - Tag: name</span></span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">func</span> <span class="title function_">createImageClassifier</span>() -&gt; <span class="type">VNCoreMLModel</span> &#123;</span><br><span class="line">        <span class="comment">// Use a default model configuration.</span></span><br><span class="line">        <span class="keyword">let</span> defaultConfig <span class="operator">=</span> <span class="type">MLModelConfiguration</span>()</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Create an instance of the image classifier&#x27;s wrapper class.</span></span><br><span class="line">        <span class="keyword">let</span> imageClassifierWrapper <span class="operator">=</span> <span class="keyword">try?</span> beatlesAndStagBeatlesClassifier(configuration: defaultConfig)　<span class="comment">// ここを書き換えています。</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">guard</span> <span class="keyword">let</span> imageClassifier <span class="operator">=</span> imageClassifierWrapper <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="built_in">fatalError</span>(<span class="string">&quot;App failed to create an image classifier model instance.&quot;</span>)</span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure>

<p>これで学習させたモデルで撮影した画像が『カブトムシ』か『〇〇クワガタ』かを識別できます。</p>
<p><strong>作成したカブトムシ・クワガタを分類するミニアプリ(再掲)</strong></p>
<p>（これを使えば、道端で遭遇したカブトムシ・クワガタの種類がわかるようになる？）</p>
<img src="/images/2023/20230831a/beatle_app_2.gif" alt="" width="177" height="384" loading="lazy">

<h2 id="さいごに">さいごに</h2><p>今回は、CreateMLをつかって機械学習モデルを作ってみることができました。しかし、分類精度はあまり良くなかったため、細部までチューニングができるCreate ML Componentsを使用してモデルを作成してみたい思います。</p>
<p>今後もSwift周りで学習した内容を投稿していきたいと思いますので、その際もお読みいただけると嬉しいです。</p>
<h2 id="参考">参考</h2><p>https://developer.apple.com/machine-learning/create-ml/</p>
<p>https://developer.apple.com/documentation/createml/</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">夏の風物詩であるカブトムシやクワガタを識別するミニアプリを作りました。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
    <category term="機械学習" scheme="https://future-architect.github.io/tags/%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92/"/>
    <category term="画像処理" scheme="https://future-architect.github.io/tags/%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86/"/>
  </entry>
  <entry>
    <title>【SwiftUI】Swift Charts についてまとめてみた</title>
    <link href="https://future-architect.github.io/articles/20230720a/"/>
    <id>https://future-architect.github.io/articles/20230720a/</id>
    <published>2023-07-19T15:00:00.000Z</published>
    <updated>2023-07-19T15:00:00.000Z</updated>
    <author><name>橋本竜我</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>はじめまして！ 2022年5月キャリア入社、HealthCare Innovation Group(HIG)<sup id="fnref:1">1</sup>の橋本です。技術ブログ初投稿です。</p>
<p>iOS16からApple標準で利用できるようになったSwift Chartsというグラフ等を作成できるフレームワークを取り上げます。</p>
<p>取り上げた理由としては、２点あります。</p>
<ol>
<li>現在のプロジェクトで活用する機会がありそうなため、事前にキャッチアップしておきたいと考えたため</li>
<li>データ分析の分野にも興味があり、後ほど投稿しよう思っているSwiftで機械学習をしたときの結果をきれいに可視化してみたいと思っているから</li>
</ol>
<p>早速本題に入っていきます。内容は次のような構成です。</p>
<h2 id="内容">内容</h2><ul>
<li>Swift Chartsとは<ul>
<li>Swift Chartsの基本的な使い方</li>
<li>グラフを描く要素を定義するデータ構造を設計する</li>
</ul>
</li>
<li>6つのMarkとその使い方<ul>
<li>AreaMask</li>
<li>LineMark</li>
<li>PointMark</li>
<li>RectangleMark</li>
<li>RuleMark</li>
<li>BarMark</li>
</ul>
</li>
<li>iPhoneのScreen Timeの表示をSwift Chartsで作ってみる</li>
</ul>
<h3 id="Swift-Chartsとは">Swift Chartsとは</h3><p>Swift Chartsとは、WWDC2022で発表されたカスタマイズされたグラフを作成できるSwiftUIフレームワークです。</p>
<p>https://developer.apple.com/documentation/charts</p>
<p>イメージとしては、以下のように様々なグラフを描くことができます。<br><img fetchpriority="high" src="/images/2023/20230720a/image.png" alt="" width="1200" height="424"></p>
<p>また、WWDC2023のセッション(Explore pie charts and interactivity in Swift Charts)で7つ目となる<code>SectorMark</code>が発表されました。<code>SectorMark</code>を使うことで簡単にPie Chartを作ることがiOS17から可能になるとのことです。</p>
<img src="/images/2023/20230720a/image_2.png" alt="" width="1200" height="472" loading="lazy">

<p>https://developer.apple.com/videos/play/wwdc2023/10037/</p>
<p>https://developer.apple.com/documentation/charts/sectormark</p>
<h4 id="Swift-Chartsの基本的な使い方">Swift Chartsの基本的な使い方</h4><p>簡単な棒グラフを作っていきます。<br>完成イメージは、縦軸が値段、横軸にフルーツが並ぶ棒グラフです。<br>使用する要素は以下のとおりです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>フルーツ</th>
<th>値段</th>
</tr>
</thead>
<tbody><tr>
<td>りんご</td>
<td>100円</td>
</tr>
<tr>
<td>オレンジ</td>
<td>50円</td>
</tr>
<tr>
<td>バナナ</td>
<td>200円</td>
</tr>
</tbody></table></div>
<p>早速作っていきます。<br>まず、Swift Chartsを利用するために、<code>Charts</code>をインポートします。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> Charts</span><br></pre></td></tr></table></figure>

<p>これによって、Swift Chartsが使えるようになりました。</p>
<p>次にグラフを描画する場所を定義します。</p>
<p>グラフを記述する場所として、View配下に<code>Chart &#123;&#125;</code>を次のように記載します。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">ContentView</span>: <span class="title class_ inherited__">View</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">Chart</span> &#123;</span><br><span class="line">            <span class="comment">// ここに後述するMarkを記載します。</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>次に、今回は棒グラフを作成するので、棒グラフを描くことができる<code>BarMark</code>を使っていきます。</p>
<p>さきほどのコードに<code>BarMark</code>を追加します。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">Chart</span> &#123;</span><br><span class="line">    <span class="type">BarMark</span>(</span><br><span class="line">        x: .value(<span class="string">&quot;fruit&quot;</span>, <span class="string">&quot;りんご&quot;</span>),</span><br><span class="line">        y: .value(<span class="string">&quot;Price&quot;</span>, <span class="number">100</span>)</span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<img src="/images/2023/20230720a/5fd9a545-cb98-f283-f114-5216698e0097.png" alt="" width="564" height="432" loading="lazy">

<p>BarMark内の<code>x: .value(&quot;Fruit&quot;, &quot;Apple&quot;)</code>でx軸のラベル自体を<code>Fruit</code>と定義し、表示されている1つのバー要素が<code>Apple</code>であることを示しています。<code>y: .value(&quot;Price&quot;, 100)</code>も同様に、y軸のラベル自体を<code>Price</code>と定義し、表示されている1つのバー要素が<code>100</code>であることを示しています。</p>
<p>”りんご” のバーを追加できましたので、オレンジ、バナナを追加します。</p>
<p>追加する方法は、とても簡単でオレンジ用、バナナ用のBarMarkをそれぞれ追加するだけです。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">Chart</span> &#123;</span><br><span class="line">    <span class="type">BarMark</span>(</span><br><span class="line">        x: .value(<span class="string">&quot;fruit&quot;</span>, <span class="string">&quot;りんご&quot;</span>),</span><br><span class="line">        y: .value(<span class="string">&quot;Price&quot;</span>, <span class="number">100</span>)</span><br><span class="line">    )</span><br><span class="line">    <span class="type">BarMark</span>(</span><br><span class="line">        x: .value(<span class="string">&quot;fruit&quot;</span>, <span class="string">&quot;オレンジ&quot;</span>),</span><br><span class="line">        y: .value(<span class="string">&quot;Price&quot;</span>, <span class="number">50</span>)</span><br><span class="line">    )</span><br><span class="line">    <span class="type">BarMark</span>(</span><br><span class="line">        x: .value(<span class="string">&quot;fruit&quot;</span>, <span class="string">&quot;バナナ&quot;</span>),</span><br><span class="line">        y: .value(<span class="string">&quot;Price&quot;</span>, <span class="number">200</span>)</span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<img src="/images/2023/20230720a/4616977c-472b-b182-8dea-58c0b452892c.png" alt="" width="558" height="430" loading="lazy">

<p>これで完成イメージ通りのグラフが完成しました。しかし、この方法ですとBarMarkを要素が1つ追加するごとに増えていくので、数が多くなると大変見づらくなってしまいます。</p>
<p>そこで、グラフの要素を構造体で定義することで<code>View</code>内を簡潔に記載できます。</p>
<h5 id="グラフを描く要素を定義するデータ構造を設計する">グラフを描く要素を定義するデータ構造を設計する</h5><p>グラフで利用するデータ構造の一般的な設計について説明します。</p>
<p>Chartsでは、ForEachのようにループ文を使うことができるため、構造体を<code>Identifiable</code>プロトコルに準拠させ、一意の<code>id</code>をプロパティとして定義しておきます。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Data</span>: <span class="title class_ inherited__">Identifiable</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> id <span class="operator">=</span> <span class="type">UUID</span>()   <span class="comment">// 一意のidを持たせる</span></span><br><span class="line">    <span class="keyword">let</span> fruitName: <span class="type">String</span></span><br><span class="line">    <span class="keyword">let</span> price: <span class="type">Double</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>これを用いて、先程紹介した棒グラフを実装すると次のようになります。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 棒グラフの各要素を定義する</span></span><br><span class="line"><span class="keyword">let</span> item:[<span class="type">Item</span>] <span class="operator">=</span> [</span><br><span class="line">    <span class="type">Item</span>(price: <span class="number">100</span>, fruitName: <span class="string">&quot;apple&quot;</span>),</span><br><span class="line">    <span class="type">Item</span>(price: <span class="number">50</span>, fruitName: <span class="string">&quot;orange&quot;</span>),</span><br><span class="line">    <span class="type">Item</span>(price: <span class="number">200</span>, fruitName: <span class="string">&quot;Banana&quot;</span>)</span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">ContentView</span>: <span class="title class_ inherited__">View</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">Chart</span>(item) &#123; element <span class="keyword">in</span></span><br><span class="line">            <span class="type">BarMark</span>(</span><br><span class="line">                x: .value(<span class="string">&quot;Fruit&quot;</span>, element.fruitName),</span><br><span class="line">                y: .value(<span class="string">&quot;Price&quot;</span>, element.price)</span><br><span class="line">            )</span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure>

<p><code>Charts</code>を<code>ForEach</code>のように利用できるため、これまでSwiftUIを触ったことがある人にはとても使いやすいと思いました。</p>
<h3 id="6つのMarkとその使い方">6つのMarkとその使い方</h3><p>6つのグラフを描画するMarkの使い方について、紹介します。<br>表に6つのMark名と主に使われるグラフとそのイメージ画像を一覧化しています。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>名前</th>
<th>説明</th>
<th>イメージ</th>
</tr>
</thead>
<tbody><tr>
<td>AreaMark</td>
<td>面グラフ</td>
<td><img src="/images/2023/20230720a/image_3.png" alt="image.png" width="312" height="214" loading="lazy"></td>
</tr>
<tr>
<td>LineMark</td>
<td>折れ線グラフ</td>
<td><img src="/images/2023/20230720a/image_4.png" alt="image.png" width="311" height="211" loading="lazy"></td>
</tr>
<tr>
<td>PointMark</td>
<td>散布図</td>
<td><img src="/images/2023/20230720a/image_5.png" alt="image.png" width="319" height="212" loading="lazy"></td>
</tr>
<tr>
<td>RectangleMark</td>
<td>ヒートマップ</td>
<td><img src="/images/2023/20230720a/image_6.png" alt="image.png" width="308" height="213" loading="lazy"></td>
</tr>
<tr>
<td>RuleMark</td>
<td>水平線、垂直線</td>
<td><img src="/images/2023/20230720a/image_7.png" alt="image.png" width="320" height="218" loading="lazy"></td>
</tr>
<tr>
<td>BarMark</td>
<td>棒グラフ</td>
<td><img src="/images/2023/20230720a/image_8.png" alt="image.png" width="314" height="213" loading="lazy"></td>
</tr>
</tbody></table></div>
<p>BarMarkの使い方は、すでに紹介しましたので、残りの5つのMarkのサンプルコードを次に記載します。</p>
<p>（ここでは基本的にデータ構造などのコードは省き、<code>Chart&#123;&#125;</code>内のコードのみを載せています）。</p>
<h4 id="Area-Markのサンプルコード">Area Markのサンプルコード</h4><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">Chart</span>(cheeseburgerCost) &#123;</span><br><span class="line">    <span class="type">AreaMark</span>(</span><br><span class="line">        x: .value(<span class="string">&quot;Date&quot;</span>, <span class="variable">$0</span>.date),</span><br><span class="line">        y: .value(<span class="string">&quot;Price&quot;</span>, <span class="variable">$0</span>.price)</span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="LineMarkのサンプルコード">LineMarkのサンプルコード</h4><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">Chart</span>(data) &#123;</span><br><span class="line">    <span class="type">LineMark</span>(</span><br><span class="line">        x: .value(<span class="string">&quot;Month&quot;</span>, <span class="variable">$0</span>.date),</span><br><span class="line">        y: .value(<span class="string">&quot;Hours of Sunshine&quot;</span>, <span class="variable">$0</span>.hoursOfSunshine)</span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="PointMarkのサンプルコード">PointMarkのサンプルコード</h4><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">Chart</span>(data) &#123;</span><br><span class="line">    <span class="type">PointMark</span>(</span><br><span class="line">        x: .value(<span class="string">&quot;Wing Length&quot;</span>, <span class="variable">$0</span>.wingLength),</span><br><span class="line">        y: .value(<span class="string">&quot;Wing Width&quot;</span>, <span class="variable">$0</span>.wingWidth)</span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="RectangleMarkのサンプルコード">RectangleMarkのサンプルコード</h4><p>このサンプルコードでは、<code>width</code>と<code>height</code>でバーエリア内の比率を1.0にすることでヒートマップを作成しています。作成されるヒートマップは、上記で記載している表中のイメージのものです。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">MatrixEntry</span>: <span class="title class_ inherited__">Identifiable</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> positive: <span class="type">String</span></span><br><span class="line">    <span class="keyword">var</span> negative: <span class="type">String</span></span><br><span class="line">    <span class="keyword">var</span> num: <span class="type">Double</span></span><br><span class="line">    <span class="keyword">let</span> id <span class="operator">=</span> <span class="type">UUID</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">var</span> data: [<span class="type">MatrixEntry</span>] <span class="operator">=</span> [</span><br><span class="line">    <span class="type">MatrixEntry</span>(positive: <span class="string">&quot;+&quot;</span>, negative: <span class="string">&quot;+&quot;</span>, num: <span class="number">125</span>),</span><br><span class="line">    <span class="type">MatrixEntry</span>(positive: <span class="string">&quot;+&quot;</span>, negative: <span class="string">&quot;-&quot;</span>, num: <span class="number">10</span>),</span><br><span class="line">    <span class="type">MatrixEntry</span>(positive: <span class="string">&quot;-&quot;</span>, negative: <span class="string">&quot;-&quot;</span>, num: <span class="number">80</span>),</span><br><span class="line">    <span class="type">MatrixEntry</span>(positive: <span class="string">&quot;-&quot;</span>, negative: <span class="string">&quot;+&quot;</span>, num: <span class="number">1</span>)</span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">RectangleMaskView</span>: <span class="title class_ inherited__">View</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">Chart</span>(data) &#123;</span><br><span class="line">            <span class="type">RectangleMark</span>(</span><br><span class="line">                x: .value(<span class="string">&quot;Positive&quot;</span>, <span class="variable">$0</span>.positive),</span><br><span class="line">                y: .value(<span class="string">&quot;Negative&quot;</span>, <span class="variable">$0</span>.negative)</span><br><span class="line">                width: .ratio(<span class="number">1</span>),</span><br><span class="line">                height: .ratio(<span class="number">1</span>)</span><br><span class="line">            )</span><br><span class="line">            .foregroundStyle(by: .value(<span class="string">&quot;Number&quot;</span>, <span class="variable">$0</span>.num))</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 凡例を非表示に設定</span></span><br><span class="line">        .chartLegend(.hidden)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Y軸のラベル位置を設定</span></span><br><span class="line">        .chartYAxis &#123;</span><br><span class="line">            <span class="type">AxisMarks</span>(preset: .aligned, position: .leading)</span><br><span class="line">        &#125;</span><br><span class="line">        .frame(width: <span class="number">300</span>, height: <span class="number">200</span>)</span><br><span class="line">        .padding()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h4 id="RuleMarkのサンプルコード">RuleMarkのサンプルコード</h4><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">var</span> data: [<span class="type">Pollen</span>] <span class="operator">=</span> [</span><br><span class="line">    <span class="type">Pollen</span>(startMonth: <span class="number">1</span>, numMonths: <span class="number">9</span>, source: <span class="string">&quot;Trees&quot;</span>),</span><br><span class="line">    <span class="type">Pollen</span>(startMonth: <span class="number">12</span>, numMonths: <span class="number">1</span>, source: <span class="string">&quot;Trees&quot;</span>),</span><br><span class="line">    <span class="type">Pollen</span>(startMonth: <span class="number">3</span>, numMonths: <span class="number">8</span>, source: <span class="string">&quot;Grass&quot;</span>),</span><br><span class="line">    <span class="type">Pollen</span>(startMonth: <span class="number">4</span>, numMonths: <span class="number">8</span>, source: <span class="string">&quot;Weeds&quot;</span>)</span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">RuleMaskView</span>: <span class="title class_ inherited__">View</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">Chart</span>(data) &#123;</span><br><span class="line">            <span class="type">RuleMark</span>(</span><br><span class="line">                xStart: .value(<span class="string">&quot;Start Date&quot;</span>, <span class="variable">$0</span>.startDate),</span><br><span class="line">                xEnd: .value(<span class="string">&quot;End Date&quot;</span>, <span class="variable">$0</span>.endDate),</span><br><span class="line">                y: .value(<span class="string">&quot;Pollen Source&quot;</span>, <span class="variable">$0</span>.source)</span><br><span class="line">            )</span><br><span class="line">        &#125;</span><br><span class="line">        .frame(width: <span class="number">300</span>, height: <span class="number">200</span>)</span><br><span class="line">        .padding()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="SectorMarkのサンプルコード-iOS17以降">SectorMarkのサンプルコード(iOS17以降)</h4><p>WWDC2023で発表された<code>SectorMark</code>のサンプルコードです。<code>BarMark</code>等で使っていた<code>x:</code>を<code>angle</code>に変えるだけで、簡単にPie chartsに変換できます。</p>
<p>また、<code>innnerRadius:</code>、<code>angularInset</code>でPie chartsをカスタマイズできます。<code>innnerRadius:</code>を使用することで、パイチャートの内部を指定の比率だけくり抜き、ドーナッツチャートにすることもできます。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">    <span class="type">Chart</span>(data, id: \.name) &#123; name, sales <span class="keyword">in</span></span><br><span class="line">        <span class="type">SectorMark</span>(</span><br><span class="line">            angle: .value(<span class="string">&quot;Value&quot;</span>, sales),</span><br><span class="line">            innerRadius: .ratio(<span class="number">0.618</span>),  <span class="comment">// Pie charts中心から指定の比率だけくり抜ける</span></span><br><span class="line">            angularInset: <span class="number">1</span> <span class="comment">// Pie chartsの各要素の間に1 pointの隙間を与える</span></span><br><span class="line">        )</span><br><span class="line">        .cornerRadius(<span class="number">4</span>)</span><br><span class="line">        .foregroundStyle(by: .value(<span class="string">&quot;Product category&quot;</span>, name))</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>再掲となりますが、以下のWWDC2023のセッション動画を見ていただけると、<code>SectorMark</code>について理解が進むと思います。</p>
<p>https://developer.apple.com/videos/play/wwdc2023/10037/</p>
<h2 id="スクリーンタイムのグラフを模倣して作ってみた">スクリーンタイムのグラフを模倣して作ってみた</h2><p>最後に、学習してきた知識を活用して、iPhoneアプリなどに標準で搭載されているスクリーンタイムのグラフを模倣してみました。</p>
<h3 id="環境">環境</h3><ul>
<li>macOS: Ventura 13.4.1</li>
<li>Xcode: Version 14.3.1 (14E300c)</li>
</ul>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">BarItem</span>: <span class="title class_ inherited__">Identifiable</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> dayOfWeek: <span class="type">String</span></span><br><span class="line">    <span class="keyword">let</span> hour: <span class="type">Double</span></span><br><span class="line">    <span class="keyword">let</span> category: <span class="type">String</span></span><br><span class="line">    <span class="keyword">let</span> id <span class="operator">=</span> <span class="type">UUID</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">let</span> data: [<span class="type">BarItem</span>] <span class="operator">=</span> [</span><br><span class="line">    <span class="type">BarItem</span>(dayOfWeek: <span class="string">&quot;日&quot;</span>, hour: <span class="number">1.5</span>, category: <span class="string">&quot;エンターテイメント&quot;</span>),</span><br><span class="line">    <span class="comment">// 省略</span></span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">ScreenTimeWidgetView</span>: <span class="title class_ inherited__">View</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> body: <span class="keyword">some</span> <span class="type">View</span> &#123;</span><br><span class="line">        <span class="type">NavigationStack</span> &#123;</span><br><span class="line">            <span class="type">List</span> &#123;</span><br><span class="line">                <span class="type">Section</span>(header: <span class="type">Text</span>(<span class="string">&quot;スクリーンタイム&quot;</span>)) &#123;</span><br><span class="line">                    <span class="type">VStack</span>(alignment: .leading, spacing: <span class="number">0</span>) &#123;</span><br><span class="line">                        <span class="type">Text</span>(<span class="string">&quot;1日の平均&quot;</span>)</span><br><span class="line">                            .font(.footnote)</span><br><span class="line">                            .foregroundColor(.secondary)</span><br><span class="line">                        <span class="type">Text</span>(<span class="string">&quot;3 時間 45 分&quot;</span>)</span><br><span class="line">                            .font(.title)</span><br><span class="line"></span><br><span class="line">                        <span class="comment">// グラフを描画</span></span><br><span class="line">                        <span class="type">Chart</span> &#123;</span><br><span class="line">                            <span class="type">ForEach</span>(data) &#123;</span><br><span class="line">                                <span class="type">BarMark</span>(</span><br><span class="line">                                    x: .value(<span class="string">&quot;Category&quot;</span>, <span class="variable">$0</span>.dayOfWeek),</span><br><span class="line">                                    y: .value(<span class="string">&quot;Profit&quot;</span>, <span class="variable">$0</span>.hour)</span><br><span class="line">                                )</span><br><span class="line">                                .foregroundStyle(<span class="variable">$0</span>.color)</span><br><span class="line">                                .foregroundStyle(by: .value(<span class="string">&quot;Product Category&quot;</span>, <span class="variable">$0</span>.category))</span><br><span class="line">                                <span class="comment">// 棒グラフの角を丸くする</span></span><br><span class="line">                                .cornerRadius(<span class="number">3</span>)</span><br><span class="line"></span><br><span class="line">                                <span class="comment">// 一週間の平均利用時間(h)</span></span><br><span class="line">                                <span class="type">RuleMark</span>(y: .value(<span class="string">&quot;平均&quot;</span>, <span class="number">3.45</span>))</span><br><span class="line">                                    .foregroundStyle(.green)</span><br><span class="line"></span><br><span class="line">                                <span class="comment">// 線のスタイルを指定。ここでは点線にしている。</span></span><br><span class="line">                                    .lineStyle(<span class="type">StrokeStyle</span>(lineWidth: <span class="number">1.5</span>, lineCap: .butt, lineJoin: .miter, dash: [<span class="number">5</span>,<span class="number">5</span>,<span class="number">5</span>,<span class="number">5</span>], dashPhase: <span class="number">0</span>))</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;</span><br><span class="line">                        .chartForegroundStyleScale([</span><br><span class="line">                            <span class="string">&quot;エンターテイメント&quot;</span>: .blue, <span class="string">&quot;仕事の効率化とファイナンス&quot;</span>: .cyan, <span class="string">&quot;SNS&quot;</span>: .orange</span><br><span class="line">                        ])</span><br><span class="line">                    &#125;</span><br><span class="line">                    .frame(width: <span class="number">300</span>, height: <span class="number">250</span>)</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<img src="/images/2023/20230720a/c82576c7-af28-9a39-950c-a5b1206ef480.png" alt="" width="872" height="828" loading="lazy">

<p>本物にかなり似たグラフを実装できたと思います。</p>
<p>より本物のスクリーンタイムに合わせるには、以下をうまく実装に反映させる必要があります。</p>
<ul>
<li><code>RuleMark</code>の点線の横に、平均という文字を入れること</li>
<li>縦軸の目盛りを調節すること</li>
<li>縦軸の目盛りに単位を追加すること</li>
<li>日、月、などの曜日をそれぞれの要素内で左に寄せること</li>
</ul>
<p>これらの課題は、時間を見つけて改善していきたいと思います。</p>
<h2 id="さいごに">さいごに</h2><p>今回は、iOS16から利用できるようになったSwift Chartsについて理解を深めました。</p>
<p>今後もSwift周りで学習した内容を投稿していきたいと思いますので、その際もお読みいただけると嬉しいです。</p>
<h2 id="参考">参考</h2><p>https://developer.apple.com/documentation/charts/areamark</p>
<p>https://developer.apple.com/documentation/charts/linemark</p>
<p>https://developer.apple.com/documentation/charts/pointmark</p>
<p>https://developer.apple.com/documentation/charts/rectanglemark</p>
<p>https://developer.apple.com/documentation/charts/rulemark</p>
<p>https://developer.apple.com/documentation/charts/barmark</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">iOS16からApple標準で利用できるようになったSwift Chartsというグラフ等を作成できるフレームワークを取り上げたいと思います。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="SwiftUI" scheme="https://future-architect.github.io/tags/SwiftUI/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
  </entry>
  <entry>
    <title>iOSアプリのCI入門</title>
    <link href="https://future-architect.github.io/articles/20230524a/"/>
    <id>https://future-architect.github.io/articles/20230524a/</id>
    <published>2023-05-23T15:00:00.000Z</published>
    <updated>2023-05-23T15:00:00.000Z</updated>
    <author><name>清水雄一郎</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>はじめまして！ 2021年10月新卒入社、HealthCare Inovation Group(HIG)所属の清水です。</p>
<p>春の入門ブログ連載の22日目は、入門ブログ連載のテーマとして、今までやってこなかったCIをやってみた記事です。</p>
<p>CIについてあまり知らない自分が、プロジェクトにCIを導入したいと思い、実際に調べてみた・試したことをまとめます。</p>
<h2 id="内容">内容</h2><ul>
<li>CIとは</li>
<li>何はともあれ試してみる</li>
<li>iOSアプリでのCI</li>
</ul>
<h2 id="CIとは">CIとは</h2><p>以下は、SaaS型のCI&#x2F;CDサービスを提供しているCircleCIさんのページからの引用です。</p>
<blockquote>
<p>継続的インテグレーション（CI）とは、Continuous Integrationの略であり、デプロイするコードの品質を確保しながら、開発スピードを向上させるDevOpsソフトウェア開発手法です。開発者は継続的にコードを少しずつ(少なくとも1日1回、可能なら1日に数回)コミットし、そのコードが共有リポジトリにマージされる前にビルドとテストが自動的に実行されます。</p>
</blockquote>
<p>https://circleci.com/ja/continuous-integration/</p>
<p>私が所属するプロジェクトでは、元々1~3人でアプリ開発を進めていました。</p>
<p>ただ最近は、機能が増えて、プロジェクトに参画する人が増えて、開発者も増えて、改修箇所も増えて、テストも増えて…という状況になっています。アプリの品質を確保しながら、チームでの開発をより安心して前に進めるために、CIを導入したいと思い試し始めました。</p>
<p>ちなみに…</p>
<p>セットになっていることが多いCDは、Continuous Delivery &amp; Deployment(継続的デリバリー及び継続的デプロイ)の略です。<br>Continuous Deliveryは、いつでもデプロイできる状態にすることを指し、デプロイ自体の操作は手動であり、Continuous Deploymentは、ビルド〜デプロイまでを自動で行うことを指すようです。</p>
<h2 id="何はともあれ試してみる">何はともあれ試してみる</h2><p>ここでは実際にSwiftで実装したiOSアプリのリポジトリをCIツールによって自動でビルド＆テストするところまで試してみます。</p>
<h3 id="前提">前提</h3><ul>
<li>macOS: Ventura 13.4</li>
<li>Xcode: Version 14.3 (14E222b)</li>
<li>CIツール: GitLab CI&#x2F;CD</li>
</ul>
<p>普段、業務ではGitLabを利用してバージョン&amp;issueを管理しているため、GitLabのCI&#x2F;CDツールを利用します。<br>今回試していく内容の大きな手順は以下です。</p>
<ol>
<li>iOSアプリのリポジトリを用意</li>
<li>MacにGitLab Runnerをセットアップ</li>
<li>リポジトリにRunnerを登録</li>
<li>リポジトリにCI用のconfigファイルを追加</li>
</ol>
<p>やっていきます。</p>
<h3 id="iOSアプリのリポジトリを用意">iOSアプリのリポジトリを用意</h3><p>検証用のリポジトリを用意します。今回は、GitLabのtemplateから作成したリポジトリを利用します。</p>
<p>食事の画像とレビューを記録するアプリのようです（初めて使いました）。</p>
<p>アプリのソースコードだけではなく、ユニットテストとUIテストのコードも最初から用意されているようです。</p>
<img fetchpriority="high" src="/images/2023/20230524a/スクリーンショット_2023-05-23_14.26.39.png" alt="" width="1200" height="675">

<p>GitLab上から見るとこのような状態です。CI&#x2F;CDのセットアップは未完了です。</p>
<img src="/images/2023/20230524a/スクリーンショット_2023-05-24_14.36.53.png" alt="" width="1200" height="611" loading="lazy">

<h3 id="MacにGitLab-Runnerをセットアップ">MacにGitLab Runnerをセットアップ</h3><p>ここでは基本的にGitLab公式の記事に従って手元のMacにビルド&amp;テストを実行するためのツールをセットアップします。<br>Homebrewを利用してインストールもできますが、GitLabがメンテナンスしていないようなので、Manual(curl)でインストールしました。</p>
<h3 id="リポジトリにRunnerを登録">リポジトリにRunnerを登録</h3><p>次に、GitLabのリポジトリにセットアップしたRunnerを登録します。</p>
<p>GitLabの日本語ブログを参考に進めました。</p>
<p>GitLabのリポジトリ &gt; CI&#x2F;CD Settings &gt; Runners から登録するためのURL（1）とトークン（2）を確認できます。</p>
<img src="/images/2023/20230524a/スクリーンショット_2023-05-24_15.00.07.png" alt="" width="1200" height="656" loading="lazy">

<p>ターミナルを立ち上げて、以下のように進めていきます（一部省略・隠蔽しています）。</p>
<figure class="highlight zsh"><table><tr><td class="code"><pre><span class="line">gitlab-runner register</span><br><span class="line">Runtime platform                                    <span class="built_in">arch</span>=amd64 os=darwin pid=12166 revision=dcfb4b66 version=15.10.1</span><br><span class="line"></span><br><span class="line">Enter the GitLab instance URL (<span class="keyword">for</span> example, https://gitlab.com/):</span><br><span class="line">// ①をコピペします。</span><br><span class="line">Enter the registration token:</span><br><span class="line">// ②をコピペします。</span><br><span class="line">Enter a description <span class="keyword">for</span> the runner:</span><br><span class="line">[XXXXX.<span class="built_in">local</span>]: ForBlogMyMac // ③Runnerの説明を加えます。</span><br><span class="line">Enter tags <span class="keyword">for</span> the runner (comma-separated):</span><br><span class="line">blog, // ④タグの設定。技術ブログ用なので、<span class="string">&quot;blog&quot;</span>タグを設定します。</span><br><span class="line">Enter optional maintenance note <span class="keyword">for</span> the runner:</span><br><span class="line">// 説明とは異なるメモ。今回は空にしている。</span><br><span class="line">Registering runner... succeeded                     runner=XXXXXX</span><br><span class="line">Enter an executor: virtualbox, docker-ssh+machine, kubernetes, docker-ssh, parallels, shell, ssh, custom, docker, docker+machine, instance:</span><br><span class="line">shell  // 実行環境。<span class="string">&quot;xcodebuild&quot;</span>を利用するため、shellを選択しています。</span><br><span class="line">Runner registered successfully. Feel free to start it, but <span class="keyword">if</span> it<span class="string">&#x27;s running already the config should be automatically reloaded!</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Configuration (with the authentication token) was saved in &quot;/Users/XXXXX/.gitlab-runner/config.toml&quot;</span></span><br></pre></td></tr></table></figure>

<p>GitLab側からRunnerが登録されていることを確認できます。</p>
<img src="/images/2023/20230524a/スクリーンショット_2023-05-24_15.00.54.png" alt="" width="1200" height="694" loading="lazy">

<h3 id="リポジトリにCI用のconfigファイルを追加">リポジトリにCI用のconfigファイルを追加</h3><p>いよいよリポジトリにCI用のファイルを追加します。</p>
<p>GitLabでは、<code>.gitlab-ci.yml</code>というYAMLファイルでCI&#x2F;CDパイプラインの構成や順序を定義してきます。</p>
<p>https://gitlab-docs.creationline.com/ee/ci/yaml/</p>
<p>Webから追加できます。</p>
<img src="/images/2023/20230524a/スクリーンショット_2023-05-24_15.22.34.png" alt="" width="983" height="723" loading="lazy">

<p>GitLabにtemplateが用意されているため、そちらを使います。</p>
<p>https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/lib/gitlab/ci/templates/Swift.gitlab-ci.yml</p>
<p>上述のtemplateでは、アプリのビルド＆テスト→アーカイブの作成→デプロイまで実行しようとしています。</p>
<figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="attr">stages:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">build</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">test</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">archive</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">deploy</span></span><br><span class="line"></span><br><span class="line"><span class="attr">build_project:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">build</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">xcodebuild</span> <span class="string">clean</span> <span class="string">-project</span> <span class="string">ProjectName.xcodeproj</span> <span class="string">-scheme</span> <span class="string">SchemeName</span> <span class="string">|</span> <span class="string">xcpretty</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">xcodebuild</span> <span class="string">test</span> <span class="string">-project</span> <span class="string">ProjectName.xcodeproj</span> <span class="string">-scheme</span> <span class="string">SchemeName</span> <span class="string">-destination</span> <span class="string">&#x27;platform=iOS Simulator,name=iPhone 8,OS=11.3&#x27;</span> <span class="string">|</span> <span class="string">xcpretty</span> <span class="string">-s</span></span><br><span class="line">  <span class="attr">tags:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">ios_11-3</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">xcode_9-3</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">macos_10-13</span></span><br><span class="line"></span><br><span class="line"><span class="attr">archive_project:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">archive</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">xcodebuild</span> <span class="string">clean</span> <span class="string">archive</span> <span class="string">-archivePath</span> <span class="string">build/ProjectName</span> <span class="string">-scheme</span> <span class="string">SchemeName</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">xcodebuild</span> <span class="string">-exportArchive</span> <span class="string">-exportFormat</span> <span class="string">ipa</span> <span class="string">-archivePath</span> <span class="string">&quot;build/ProjectName.xcarchive&quot;</span> <span class="string">-exportPath</span> <span class="string">&quot;build/ProjectName.ipa&quot;</span> <span class="string">-exportProvisioningProfile</span> <span class="string">&quot;ProvisioningProfileName&quot;</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">$CI_COMMIT_BRANCH</span> <span class="string">==</span> <span class="string">$CI_DEFAULT_BRANCH</span></span><br><span class="line">  <span class="attr">artifacts:</span></span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">build/ProjectName.ipa</span></span><br><span class="line">  <span class="attr">tags:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">ios_11-3</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">xcode_9-3</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">macos_10-13</span></span><br><span class="line"></span><br><span class="line"><span class="attr">deploy:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy</span></span><br><span class="line">  <span class="attr">script:</span> <span class="string">echo</span> <span class="string">&quot;Define your deployment script!&quot;</span></span><br><span class="line">  <span class="attr">environment:</span> <span class="string">production</span></span><br></pre></td></tr></table></figure>

<p>今回は、最低限のビルド＆テストのみ実行したいので、以下のように記述しました。</p>
<figure class="highlight yml"><table><tr><td class="code"><pre><span class="line"><span class="attr">stages:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">build</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">test</span></span><br><span class="line"></span><br><span class="line"><span class="attr">build_project:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">build</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">xcodebuild</span> <span class="string">clean</span> <span class="string">-project</span> <span class="string">iOSTemplate.xcodeproj</span> <span class="string">-scheme</span> <span class="string">iOSTemplate</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">xcodebuild</span> <span class="string">test</span> <span class="string">-project</span> <span class="string">iOSTemplate.xcodeproj</span> <span class="string">-scheme</span> <span class="string">iOSTemplate</span> <span class="string">-destination</span> <span class="string">&#x27;platform=iOS Simulator,name=iPhone 14 Pro,OS=16.4&#x27;</span></span><br><span class="line">  <span class="attr">tags:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">blog</span></span><br></pre></td></tr></table></figure>

<p>templateから大きく違う点は、以下4点です。</p>
<ul>
<li>アーカイブとデプロイを削除</li>
<li><code>xcpretty</code>コマンドを削除（※xcprettyはログを見やすくするライブラリらしいです）。</li>
<li>テストを実行するシミュレータを最新のもの（”iPhone 14 Pro,OS&#x3D;16.4”）に変更</li>
<li>tagを登録した<code>blog</code>に設定<ul>
<li>事前に登録したtagに含まれるものでないと実行されないため注意</li>
</ul>
</li>
</ul>
<p><code>.gitlab-ci.yml</code>を追加完了すると、処理が実行開始します（手元のMacBookのファンが元気になり、物理的にも実行しているのが感じられました）。</p>
<img src="/images/2023/20230524a/スクリーンショット_2023-05-24_15.37.44.png" alt="" width="1200" height="158" loading="lazy">

<p>最小限の構成で、2分ちょっとで完了しました。</p>
<img src="/images/2023/20230524a/スクリーンショット_2023-05-24_15.45.16.png" alt="" width="1200" height="161" loading="lazy">

<h4 id="ちなみに">ちなみに</h4><p>誤ったテストを追加して<code>master</code>にpushしたところ、ちゃんと失敗になってくれました。</p>
<img src="/images/2023/20230524a/スクリーンショット_2023-05-24_16.02.22.png" alt="" width="1005" height="270" loading="lazy">
<img src="/images/2023/20230524a/スクリーンショット_2023-05-24_16.01.03.png" alt="" width="1200" height="214" loading="lazy">

<h2 id="iOSアプリでのCI-CD">iOSアプリでのCI&#x2F;CD</h2><p>本記事では、個人のリポジトリに対して、個人PCをRunnerとして登録してCIを実行しました。</p>
<p>しかし、本記事で設定したリポジトリでチーム開発を進めた場合、リポジトリに変更があるたび登録したPCがCIを実行し続けます。その際にはCI用の実行環境を、自前で用意する（オンプレミス）か、サービスとして提供されているものを利用する必要があります。</p>
<p>ただし、Swiftで実装したiOSアプリをビルドするためには、Xcodeが必要であり、そのためにはmacOSの実行環境が必要となります。</p>
<p>今回は、ツールの選定と検証まで辿り着かなかったですが、今後、業務にCIを導入した際は、CI&#x2F;CDツール選定とその経緯を記事にしたいと思っています。</p>
<h2 id="おわりに">おわりに</h2><p>本記事では、CIについて知らない状態からCI実行までを入門してみました。</p>
<p>今後は、他のツールについて調査・検証して業務に導入し、アプリの品質向上に活用していきたいです。</p>
<p>次は齊藤さんのAmazon OpenSearch Serverless を触ってみたです。</p>
]]></content>
    <summary type="html">CIについてあまり知らない自分が、プロジェクトにCIを導入したいと思い、実際に調べてみた・試したことをまとめたいと思います。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="CI/CD" scheme="https://future-architect.github.io/tags/CI-CD/"/>
    <category term="GitLab" scheme="https://future-architect.github.io/tags/GitLab/"/>
    <category term="GitLab CI" scheme="https://future-architect.github.io/tags/GitLab-CI/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
  </entry>
  <entry>
    <title>Swiftの自動テスト〜導入と基本的なテスト手法〜</title>
    <link href="https://future-architect.github.io/articles/20230515a/"/>
    <id>https://future-architect.github.io/articles/20230515a/</id>
    <published>2023-05-14T15:00:00.000Z</published>
    <updated>2023-05-14T15:00:00.000Z</updated>
    <author><name>寒河江功悟</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>はじめまして、2023年3月キャリア入社、HealthCare Inovation Group(HIG)所属の寒河江です。<br>春の入門ブログ連載の16日目です。</p>
<p>本記事はSwiftでの自動テストについての入門記事です。SwiftはXcodeを用いてiOSアプリが作成でき、自作して手元の端末で動きが見やすいため初心者がモチベーション維持したまま開発できる良い題材かなと思います。「作って動いて楽しいなぁ」→「テストしてみよう！」→ <strong>「楽にできる方法ないかな？」</strong> と思った方に見ていただければです。</p>
<p>簡単なメモアプリを作成し、それに対して部分的にテストコードを書いてみたので早速紹介していきます。</p>
<h2 id="作ったメモアプリ">作ったメモアプリ</h2><p>https://github.com/SagaeKugo/CRUD</p>
<img fetchpriority="high" src="/images/2023/20230515a/メモ動画.gif" alt="メモ動画.gif" width="360" height="240">

<h2 id="XCTestのセットアップ">XCTestのセットアップ</h2><p>今回はプロジェクト作成後にテストを追加します。</p>
<p>Xcodeを開いてFile→New→Targetを選択し</p>
<img src="/images/2023/20230515a/スクリーンショット_2023-05-15_12.24.45.png" alt="" width="1144" height="708" loading="lazy">

<p>対象となるテストを追加します。今回はUT&#x2F;UIテストどちらも実施するためどちらも追加します。</p>
<img src="/images/2023/20230515a/スクリーンショット_2023-05-15_12.24.03.png" alt="" width="1200" height="858" loading="lazy">

<p>テスト用のフォルダが作成され、TARGETにテストが追加されました。</p>
<img src="/images/2023/20230515a/スクリーンショット_2023-05-15_12.31.48.png" alt="" width="966" height="644" loading="lazy">

<h2 id="UT対象">UT対象</h2><p>例のため、極端に簡素化した関数を作成してテストします。</p>
<figure class="highlight swift"><figcaption><span>Utils.swift</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> Foundation</span><br><span class="line"></span><br><span class="line"><span class="comment">// テスト用足し算関数</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">calcAdd</span>(<span class="params">a</span>: <span class="type">Int</span> , <span class="params">b</span>: <span class="type">Int</span>)  -&gt; <span class="type">Int</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> a <span class="operator">+</span> b</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// テスト引き算関数</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">calcSub</span>(<span class="params">a</span>: <span class="type">Int</span> , <span class="params">b</span>: <span class="type">Int</span>)  -&gt; <span class="type">Int</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> a <span class="operator">-</span> b</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// テスト用割り算関数</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">calcDiv</span>(<span class="params">a</span>: <span class="type">Int</span> , <span class="params">b</span>: <span class="type">Int</span>)  -&gt; <span class="type">Int</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> a <span class="operator">/</span> b</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// テスト用掛け算関数</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">calcMulti</span>(<span class="params">a</span>: <span class="type">Int</span> , <span class="params">b</span>: <span class="type">Int</span>)  -&gt; <span class="type">Int</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> a <span class="operator">*</span> b</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="UTの実施（1）〜デフォルトで入っている関数の扱い〜">UTの実施（1）〜デフォルトで入っている関数の扱い〜</h2><p>先ほどUnit Testing Bungleを選択して追加したCRUDTestsにテストコードを記載していきます。</p>
<figure class="highlight swift"><figcaption><span>CRUDTests.swift</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> XCTest</span><br><span class="line"><span class="keyword">@testable</span> <span class="keyword">import</span> CRUD</span><br><span class="line"></span><br><span class="line"><span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">CRUDTests</span>: <span class="title class_ inherited__">XCTestCase</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">override</span> <span class="keyword">func</span> <span class="title function_">setUpWithError</span>() <span class="keyword">throws</span> &#123;</span><br><span class="line">        <span class="comment">// Put setup code here. This method is called before the invocation of each test method in the class.</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">override</span> <span class="keyword">func</span> <span class="title function_">tearDownWithError</span>() <span class="keyword">throws</span> &#123;</span><br><span class="line">        <span class="comment">// Put teardown code here. This method is called after the invocation of each test method in the class.</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">testExample</span>() <span class="keyword">throws</span> &#123;</span><br><span class="line">        <span class="comment">// This is an example of a functional test case.</span></span><br><span class="line">        <span class="comment">// Use XCTAssert and related functions to verify your tests produce the correct results.</span></span><br><span class="line">        <span class="comment">// Any test you write for XCTest can be annotated as throws and async.</span></span><br><span class="line">        <span class="comment">// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.</span></span><br><span class="line">        <span class="comment">// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">testPerformanceExample</span>() <span class="keyword">throws</span> &#123;</span><br><span class="line">        <span class="comment">// This is an example of a performance test case.</span></span><br><span class="line">        measure &#123;</span><br><span class="line">            <span class="comment">// Put the code you want to measure the time of here.</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 足し算</span></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">testCulcAdd</span>() <span class="keyword">throws</span> &#123;</span><br><span class="line">        <span class="type">XCTAssertEqual</span>(<span class="type">CRUD</span>.calcAdd(a: <span class="number">27</span> , b: <span class="number">2</span>) , <span class="number">29</span>)</span><br><span class="line">        <span class="type">XCTAssertEqual</span>(<span class="type">CRUD</span>.calcAdd(a: <span class="number">2</span> , b: <span class="number">2</span>) , <span class="number">4</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 引き算</span></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">testCulcSub</span>() <span class="keyword">throws</span> &#123;</span><br><span class="line">        <span class="type">XCTAssertEqual</span>(<span class="type">CRUD</span>.calcSub(a: <span class="number">27</span> , b: <span class="number">2</span>) , <span class="number">25</span>)</span><br><span class="line">        <span class="type">XCTAssertEqual</span>(<span class="type">CRUD</span>.calcSub(a: <span class="number">2</span> , b: <span class="number">22</span>) , <span class="operator">-</span><span class="number">20</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 割り算</span></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">testCulcDiv</span>() <span class="keyword">throws</span> &#123;</span><br><span class="line">        <span class="type">XCTAssertEqual</span>(<span class="type">CRUD</span>.calcDiv(a: <span class="number">27</span> , b: <span class="number">2</span>) , <span class="number">13</span>)</span><br><span class="line">        <span class="type">XCTAssertEqual</span>(<span class="type">CRUD</span>.calcDiv(a: <span class="number">2</span> , b: <span class="number">2</span>) , <span class="number">1</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 掛け算</span></span><br><span class="line">    <span class="keyword">func</span> <span class="title function_">testCulcMulti</span>()<span class="keyword">throws</span> &#123;</span><br><span class="line">        <span class="type">XCTAssertEqual</span>(<span class="type">CRUD</span>.calcMulti(a: <span class="number">27</span> , b: <span class="number">2</span>) , <span class="number">54</span>)</span><br><span class="line">        <span class="type">XCTAssertEqual</span>(<span class="type">CRUD</span>.calcMulti(a: <span class="number">3</span> , b: <span class="number">2</span>) , <span class="number">6</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>デフォルトで4つのテスト関数が記載されていますが、これらはざっと以下の意味で、今回は変更する必要はないので無視します。</p>
<ul>
<li><code>setUpWithError()</code><ul>
<li>→各テストを実行する前に毎回呼ばれます。</li>
</ul>
</li>
<li>tearDownWithError()&#96;<ul>
<li>→各テスト実行後に毎回呼ばれます。</li>
</ul>
</li>
<li><code>testExample()</code><ul>
<li>→テストコードのサンプルです。&lt;test + テスト対象のメソッド名&gt;</li>
</ul>
</li>
<li><code>testPerformanceExample()</code><ul>
<li>→パフォーマンステストのサンプルです。&lt;testPerformance + テスト対象のメソッド名&gt;</li>
</ul>
</li>
</ul>
<p>そのため、XCTestCaseをまとめてテストを回すと、</p>
<ol>
<li><code>setUpWithError()</code></li>
<li><code>testExample()</code></li>
<li><code>tearDownWithError()</code></li>
<li><code>setUpWithError()</code></li>
<li><code>testCulcAdd()</code></li>
<li><code>tearDownWithError()</code></li>
</ol>
<p>…という流れでテストが動きます。</p>
<h2 id="UTの実施（2）〜関数の追加、テスト実行〜">UTの実施（2）〜関数の追加、テスト実行〜</h2><p><code>@testable import CRUD</code>と記載することでCRUDアプリ内のpublic,internalシンボルのテストが可能になります。</p>
<p>Sampleアプリを作成した場合は<code>@testable import Sample</code>になりますね。</p>
<p>続いてテスト関数を作成します。</p>
<p>内容は特筆すべき部分はないと思うので省略(参考サイトは末尾に記載)しますが、テスト関数を作成する際は必ず先頭に<code>test</code>と入れる必要があります。<br>先頭にtestと記載することでテストメソッドとして認識され、テストできる関数には左側に菱形マークがつきます。</p>
<img src="/images/2023/20230515a/スクリーンショット_2023-05-15_13.08.18.png" alt="" width="890" height="150" loading="lazy">

<img src="/images/2023/20230515a/スクリーンショット_2023-05-15_13.09.07.png" alt="" width="898" height="152" loading="lazy">

<p>それぞれの関数単位で菱形ボタンを押してテスト実行できますし、CRUDTestsクラスの菱形ボタンを押すことでクラス内のテストをまとめて実施できます。</p>
<p>成功すると緑のチェックがつき、失敗した場合は赤くバツが出てくれます。</p>
<img src="/images/2023/20230515a/スクリーンショット_2023-05-15_13.14.12.png" alt="" width="1200" height="771" loading="lazy">

<img src="/images/2023/20230515a/スクリーンショット_2023-05-15_13.17.52.png" alt="" width="1200" height="555" loading="lazy">

<p>失敗した時のログはこちらから確認できます。</p>
<img src="/images/2023/20230515a/スクリーンショット_2023-05-15_13.25.31.png" alt="" width="1200" height="441" loading="lazy">

<h2 id="UIテストの実施（1）">UIテストの実施（1）</h2><p>UI Testing Bungleを選択して追加したCRUDUITestsにテストコードを記載していきます。</p>
<p>デフォルトで<code>CRUDUITests.swift</code>と<code>CRUDUITestsLaunchTests.swift</code>の2ファイル作成されていますが、今回はCRUDUITests.swiftにテストを記載します。デフォルトで書かれているテスト関数はUTで紹介しているものと同じ役割なので省略します。</p>
<h2 id="UIテストの実施（2）">UIテストの実施（2）</h2><p>今回テストしたのは下記の１関数のみです。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">func</span> <span class="title function_">testAddDelete</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> app <span class="operator">=</span> <span class="type">XCUIApplication</span>()</span><br><span class="line">    <span class="type">XCTContext</span>.runActivity(named: <span class="string">&quot;Launch app&quot;</span>) &#123; <span class="keyword">_</span> <span class="keyword">in</span></span><br><span class="line">        app.launch()</span><br><span class="line">    &#125;</span><br><span class="line">    app.buttons[<span class="string">&quot;testAdd&quot;</span>].firstMatch.tap()</span><br><span class="line">    sleep(<span class="number">1</span>)</span><br><span class="line">    app.buttons[<span class="string">&quot;testDelete&quot;</span>].firstMatch.tap()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>testAddボタンを押下することでメモを１０件登録し、testDeleteボタンを押下することでメモを全件削除しています。</p>
<p>ユーザーが操作せずに見た目が変わることだけを確認したいためメモ内容はチェック対象外としています。メモ内容をテストする場合は別途アサーションを追加しましょう。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> app <span class="operator">=</span> <span class="type">XCUIApplication</span>()</span><br></pre></td></tr></table></figure>

<p>まずはテスト対象のアプリ<code>XCUIApplication()</code>のインスタンスを作成します。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">XCTContext</span>.runActivity(named: <span class="string">&quot;Launch app&quot;</span>) &#123; <span class="keyword">_</span> <span class="keyword">in</span></span><br><span class="line">    app.launch()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>今回の規模であれば不要ですが、<code>XCTContext.runActivity(named: &quot;~~&quot;)</code>を使って長いテストメソッドを名前付きの小さなサブステップに分割できます。<code>app.launch()</code>はnamedに記載されている通り、アプリを起動しています。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">app.buttons[<span class="string">&quot;testAdd&quot;</span>].firstMatch.tap()</span><br><span class="line">sleep(<span class="number">1</span>)</span><br><span class="line">app.buttons[<span class="string">&quot;testDelete&quot;</span>].firstMatch.tap()</span><br></pre></td></tr></table></figure>

<p>テスト対象のアプリ内の<code>testAdd</code>、<code>testDelete</code>というラベルのついたボタンを検索し、最初に見つかったものをタップしています。複数同じ名前のボタンがある場合は、<code>accessibilityIdentifier</code>を設定することで分類できます。(参考サイトにリンク記載)</p>
<h2 id="UIテスト動画">UIテスト動画</h2><p>マウス操作しなくてもシミュレータ画面左上のtestAdd,testDeleteボタンが押下されています。</p>
<img src="/images/2023/20230515a/UItest動画.gif" alt="UItest動画" width="360" height="240" loading="lazy">

<h2 id="さいごに">さいごに</h2><p>長い記事になってしまいましたが、読んでくださった方ありがとうございます。</p>
<p>「はじめに」にも書きましたがiOSアプリは普段使う分実機テストもしやすく自分の欲しいものを作ることもできるので、初心者がモチベーションを維持しながら技術を身につけるにはいい題材だと思います。(私も初心者なので書いて遊んでます)</p>
<p>今回紹介できませんでしたが、CoreDataというのを使ってサーバーを立てなくてもアプリ上で扱うデータのCRUD処理が簡単にできたりと面倒ごとが少ないのも利点です。せっかく技術を身につけるなら動かして楽しんでスキルアップしていきましょう！！</p>
<p>次は斎藤賢太さんのJSパッケージ管理ツールpnpmの概要と内部構造を眺める です。</p>
<h2 id="参考">参考</h2><ul>
<li>XCTestで使えるAssert一覧<br>XCTestのAssert一覧</li>
<li>accessibilityIdentifierの説明<br>XCUIElementの使い方をざっくりまとめてみた</li>
</ul>
]]></content>
    <summary type="html">Swiftでの自動テストについての入門記事です。SwiftはXcodeを用いてiOSアプリが作成でき、自作して手元の端末で動きが見やすいため初心者がモチベーション維持したまま開発できる良い題材かなと思っています。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Swift" scheme="https://future-architect.github.io/tags/Swift/"/>
    <category term="iOS" scheme="https://future-architect.github.io/tags/iOS/"/>
    <category term="テスト" scheme="https://future-architect.github.io/tags/%E3%83%86%E3%82%B9%E3%83%88/"/>
    <category term="初心者向け" scheme="https://future-architect.github.io/tags/%E5%88%9D%E5%BF%83%E8%80%85%E5%90%91%E3%81%91/"/>
  </entry>
  <entry>
    <title>夏の自由研究_英単語学習アプリを作ってみた</title>
    <link href="https://future-architect.github.io/articles/20220822b/"/>
    <id>https://future-architect.github.io/articles/20220822b/</id>
    <published>2022-08-21T15:00:01.000Z</published>
    <updated>2022-08-21T15:00:01.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>夏の自由研究ブログ連載2022の1本目です。</p>
<p>長女の勉強を見ていて、英単語の暗記に苦労していたのでFlutterで学習アプリを作りました。</p>
<p>英語はフォニックスで学ぶといいよ、とか最近言われたりします。確かに自分の経験から見ても、ある程度単語を覚えてくると、だいたい音節の区切りが見えてきて、「この発音だと母音の形式はだいたいこれかな？」「子音はこれかな？」とか選択肢が見えてきて、その組み合わせで考えていけるので、徐々に少ない労力でたくさんの単語を覚えていけます。</p>
<p>しかし、小学生の英語だと、まず覚えている単語が少なく、音節やフォニックスといった規則性を見つけ出すのは困難のようです。そのため、単語のスペルを覚える労力はO(n)というか、勘も働かずにほんと力技になってしまいます。あと、勘が働かないのはもう1つ問題があって、間違ったスペルを書いても「間違った」という感触がないため、間違ったスペルを何度もノートに書いて練習してしまっても気付きにくいという問題がありました。</p>
<p>あとは子供の性格や学習スタイルの個性もあると思いますが、うちの子はPCのキーボードにはそれほど抵抗がないのと、文字をたくさん書くのはストレスのようなので、PCで目と耳を使って学習、なおかつスペルのミスをなるべく間違えた瞬間にフィードバックしてあげるようなものがあれば良いかな、と思いました。</p>
<p>で、良いスマホアプリとかないかな、と思って探したのですが、今使っている英語の教材の進度に合わせた単語の選択ができるものとかがなくて、自分で作ることにしました。</p>
<h2 id="作ったアプリ">作ったアプリ</h2><p>Flutterで作りました。Flutterは案件でモバイルアプリを作ったりもして、生産性の高さは実感していました。あと、スペルとかを声で言ってくれるようなアプリが作りたかったので、当初はmacOSのsayコマンドをサブプロセス実行するようなものとして作りました。そのため、デスクトップアプリにしたかったというのもあります。</p>
<p>まあ、Text To SpeechはブラウザのAPIも後からあるのに気づいたし、最終的にはsayコマンドを実行するのはやめてFlutterのライブラリのflutter_ttsにしたので、声を出すだけならFlutter出なくても良かったというのは気付きましたが、基本的なところは1晩で作れたので技術選択は悪くはなかったかと思います。</p>
<p>ソースコードはこちらにアップしてます。</p>
<p>基本的には問題の答えをキーボードでタイプしていくだけですが、間違ったらタイプできないようにして、間違ったスペルを覚えないようにしています。ちなみに一文字でもタイプミスすると不正解になって、正解するまでは問題が出続けるというスパルタン仕様です。</p>
<img fetchpriority="high" src="/images/2022/20220822b/スクリーンショット_2022-07-09_14.29.04.png" alt="" width="1200" height="699">

<p>間違ったらスペルを声で教えてくれる、間違った箇所を色付けするようにしたあたりが工夫ポイントです。Text To Speechエンジンに<code>baseball</code>というテキストを渡せば「ベースボール」と読んでくれますが、<code>b a s e b a l l</code>と間にスペースを挟むとスペルの読み上げになりますね。今のところ試したエンジンはみんなこうなってくれました。</p>
<img src="/images/2022/20220822b/スクリーンショット_2022-07-09_14.25.36.png" alt="" width="1200" height="699" loading="lazy">

<p>最初はローカルのJSONファイルを手修正してましたが、問題編集機能もつけてみました。</p>
<img src="/images/2022/20220822b/スクリーンショット_2022-07-09_14.24.47.png" alt="" width="1200" height="621" loading="lazy">

<h2 id="今後やりたいこと">今後やりたいこと</h2><p>英単語を自動で音節(syllable)に分けてあげて、その単位で単語を分けて表示してあげると、勘が働きにくい部分をアプリとしてサポートしやすくなるんじゃないかと思ったのですが、本格的に音節分割するには何かしらの辞書データが必要そうなので、ちょっと手間が大変そうでした。簡易的なロジックは調べればいくつか出るので、そこはやってもいいかなと思っています。</p>
<p>あとは間違った回数の履歴をもとに出てくる単語の数を調整とかしたらいいかな、と思いつつ、学習曲線とかをアルゴリズムに落とし込むのは結構面倒ですね、と思ってまだやってません。いつかやりたい。</p>
<h2 id="Flutterの落とし穴">Flutterの落とし穴</h2><p>最終的にはビルドしたものをそのまま子供のPC(intel mac)にインストールしたので問題はないのですが、今後のARM macだとDeveloper Programに入らないとインストール面倒そうだな、ということでFlutter Webにしようかなと少し思ったのですが、Flutter WebにするにはNamed Routeにしないとページ遷移できません。で、Named Routeの場合はページ間で状態を渡すには、riverpodとかのステート管理ライブラリが何か必要となります。</p>
<p>今回はスピード重視で<code>Navigator.pushReplacement()</code>でコンストラクタ渡しで状態を渡すように最初に作ってしまったため、ウェブ化はできませんでした。</p>
<p>あと、当初はText To Speechのライブラリとしてtext_to_speechを使おうとしたのですが、Flutter 3でビルドが通らなかったり、依存ライブラリの互換性対応はなかなか難しいですね。人気があって更新が頻繁なライブラリを選ぶしかなさげ。</p>
<h2 id="まとめ">まとめ</h2><p>新型コロナは社会的にいろいろ大きな影響を与えましたが、リモートワークで子供の勉強を見る機会は増えました。フューチャーは今のところリモートをやめようという話はないですし、その点は良かったなと思っています。子供の得意な勉強法は姉妹であっても全然違うし、「こうすればOK」と1つのやり方だけすれば良いということもなく、無理やり特定のやり方を押し付けると学習効率が上がらないばかりか、勉強嫌いになってしまう恐れがあります。</p>
<p>今回は子供の勉強の様子をじっくり観察できたので、子供にあわせた学習アプリを作れました。Flutterはすばやくデスクトップアプリが作れて良いですね。他の科目もいくつか「ここさえクリアできれば」というポイントがいくつかあるので、また色々作ってあげようと思っています。次はsolid.jsでも使ってみようかな？ sveltekitでもいいかも？ それか、Swiftでネイティブで作るか。</p>
]]></content>
    <summary type="html">長女の勉強を見ていて、英単語の暗記に苦労していたのでFlutterで学習アプリを作りました。子供の性格や学習スタイルの個性もあると思いますが、うちの子はパソコンのキーボードにはそれほど抵抗がないのと、文字をたくさん書くのはストレスのようなので、パソコンで目と耳を使って学習、なおかつスペルのミスをなるべく間違えた瞬間にフィードバックしてあげるようなものがあれば良いかな、と思いました。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Dart" scheme="https://future-architect.github.io/tags/Dart/"/>
    <category term="Flutter" scheme="https://future-architect.github.io/tags/Flutter/"/>
  </entry>
  <entry>
    <title>Future Tech Night #22 Flutter勉強会より：Flutterアプリのperformance測定と基本的な実装ポイント</title>
    <link href="https://future-architect.github.io/articles/20220525a/"/>
    <id>https://future-architect.github.io/articles/20220525a/</id>
    <published>2022-05-24T15:00:00.000Z</published>
    <updated>2022-05-24T15:00:00.000Z</updated>
    <author><name>藤田春佳</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは。TIGの藤田です。Future Tech Night #22 Flutter勉強会にてお話した「Flutterアプリのperformance測定と基本的な実装ポイント」についてご紹介します。</p>
<h2 id="Performanceについて">Performanceについて</h2><p>複数の観点があります。Flutter GitHub リポジトリではPerformance issue を４カテゴリに分けて管理しています： perf: speed, perf: memory, perf: app size, perf: energy.<br>“Speed”には、レンダリング速度、ファイルI&#x2F;Oなども影響します。</p>
<h2 id="DevToolsについて">DevToolsについて</h2><p>公式ページでも紹介されるように、<code>Widget inspector</code>, CPU profiler, Memory view, Performance view, Network view, Logging view, Debug機能を持っています。勉強会では、Performance viewと、Memory viewを紹介しました。<br><img fetchpriority="high" src="/images/2022/20220525a/image.png" alt="DevToolsについて" width="1200" height="674"></p>
<p>以前に技術ブログで紹介したPerformance viewの使い方の振り返りにもなりましたが、勉強会では、さらに実装上の注意点をデモを交えてお話しました。<br><img src="/images/2022/20220525a/image_2.png" alt="DevToolsによる性能測定(Performance View)" width="1200" height="676" loading="lazy"></p>
<img src="/images/2022/20220525a/image_3.png" alt="Frameと処理時間" width="1200" height="675" loading="lazy">

<p>レンダリングPerformanceについての実装上の注意の基本的な3点を紹介しました。3番目については、デモとコードを用いてお話しています。<br><img src="/images/2022/20220525a/image_4.png" alt="レンダリングPerformance tips" width="1200" height="676" loading="lazy"></p>
<p>Memory viewの紹介もしています。機能は大きく分けると以下の2つで、１）Memory 使用量とイベントの時系列グラフ表示、２）Memory上のインスタンス分析（SnapShot時の分析と、trace分析の2種類）になります。<br><img src="/images/2022/20220525a/image_5.png" alt="DevToolsによる性能測定 Memory View" width="1200" height="676" loading="lazy"></p>
<h2 id="レンダリングのプロファイリングデモ">レンダリングのプロファイリングデモ</h2><p>レンダリングPerformance Tips「３）大きなGridやListは画面表示部のみをBuildする」の例をDevToolsの使い方と合わせて紹介しました。Animationを含む100個のウィジェットをスクロール表示するために、1つめはColumnとSingleChildScrollViewを使用、2つめはListView.builderを使用しています。</p>
<p>１）ColumnとSingleChildScrollViewを使用（アンチパターン）</p>
<figure class="highlight dart"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&#x27;dart:math&#x27;</span> <span class="keyword">as</span> math;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;package:flutter/material.dart&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SCScrollViewPage</span> <span class="keyword">extends</span> <span class="title">StatefulWidget</span> </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> SCScrollViewPage(&#123;Key? key&#125;) : <span class="keyword">super</span>(key: key);</span><br><span class="line">  <span class="meta">@override</span></span><br><span class="line">  _SCScrollViewPageState createState() =&gt; _SCScrollViewPageState();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">_SCScrollViewPageState</span> <span class="keyword">extends</span> <span class="title">State</span>&lt;<span class="title">SCScrollViewPage</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">static</span> <span class="keyword">const</span> _itemCount = <span class="number">100</span>;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@override</span></span><br><span class="line">  Widget build(BuildContext context) &#123;</span><br><span class="line">    <span class="keyword">var</span> screenSize = MediaQuery.of(context).size;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> Scaffold(</span><br><span class="line">      appBar: AppBar(</span><br><span class="line">        title: <span class="keyword">const</span> Text(<span class="string">&#x27;SingleChildScrollView List&#x27;</span>),</span><br><span class="line">      ),</span><br><span class="line">      body: Center(</span><br><span class="line">        child: SizedBox(</span><br><span class="line">          width: screenSize.width * <span class="number">0.6</span>,</span><br><span class="line">          height: screenSize.width * <span class="number">0.8</span>,</span><br><span class="line">          child: SingleChildScrollView(</span><br><span class="line">            child: Column(</span><br><span class="line">              children: &lt;Widget&gt;[</span><br><span class="line">                <span class="keyword">for</span> (<span class="built_in">int</span> i = <span class="number">0</span>; i &lt; _itemCount; i++) <span class="keyword">const</span> Item(),</span><br><span class="line">              ],</span><br><span class="line">            ),</span><br><span class="line">          ),</span><br><span class="line">        ),</span><br><span class="line">      ),</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Item</span> <span class="keyword">extends</span> <span class="title">StatefulWidget</span> </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> Item(&#123;Key? key&#125;) : <span class="keyword">super</span>(key: key);</span><br><span class="line">  <span class="meta">@override</span></span><br><span class="line">  State&lt;Item&gt; createState() =&gt; _ItemState();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">_ItemState</span> <span class="keyword">extends</span> <span class="title">State</span>&lt;<span class="title">Item</span>&gt; <span class="title">with</span> <span class="title">SingleTickerProviderStateMixin</span> </span>&#123;</span><br><span class="line">  <span class="keyword">late</span> <span class="keyword">final</span> _controller = AnimationController(</span><br><span class="line">    vsync: <span class="keyword">this</span>,</span><br><span class="line">    duration: <span class="keyword">const</span> <span class="built_in">Duration</span>(seconds: <span class="number">10</span>),</span><br><span class="line">  )..repeat();</span><br><span class="line"></span><br><span class="line">  <span class="meta">@override</span></span><br><span class="line">  <span class="keyword">void</span> dispose() &#123;</span><br><span class="line">    _controller.dispose();</span><br><span class="line">    <span class="keyword">super</span>.dispose();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@override</span></span><br><span class="line">  Widget build(BuildContext context) &#123;</span><br><span class="line">    <span class="keyword">return</span> SizedBox(</span><br><span class="line">      height: <span class="number">200</span>,</span><br><span class="line">      child: AnimatedBuilder(</span><br><span class="line">        animation: _controller,</span><br><span class="line">        child: Image.asset(<span class="string">&#x27;assets/images/dash.png&#x27;</span>, fit: BoxFit.fitHeight),</span><br><span class="line">        builder: (_, Widget? child) &#123;</span><br><span class="line">          <span class="keyword">return</span> Transform.rotate(</span><br><span class="line">            angle: _controller.value * <span class="number">2.0</span> * math.pi,</span><br><span class="line">            child: child,</span><br><span class="line">          );</span><br><span class="line">        &#125;,</span><br><span class="line">      ),</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>DevToolsのPerformance viewで確認すると、Frame毎に、画面に表示されないウィジェットもすべてBuildされてしまっています。<br><img src="/images/2022/20220525a/build_100loop.gif" alt="DevToolsのPerformance viewで確認" width="1200" height="442" loading="lazy"></p>
<p>２）ListView.builderを使用<br>上の１）と同じItem ウィジェットを、ListView.builderを使用して表示すると、Frame毎に、画面に表示される数個のウィジェットのみがBuildされることが分かります。</p>
<figure class="highlight dart"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&#x27;dart:math&#x27;</span> <span class="keyword">as</span> math;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;package:flutter/material.dart&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">LVBuilderPage</span> <span class="keyword">extends</span> <span class="title">StatefulWidget</span> </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> LVBuilderPage(&#123;Key? key&#125;) : <span class="keyword">super</span>(key: key);</span><br><span class="line">  <span class="meta">@override</span></span><br><span class="line">  _LVBuilderPageState createState() =&gt; _LVBuilderPageState();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">_LVBuilderPageState</span> <span class="keyword">extends</span> <span class="title">State</span>&lt;<span class="title">LVBuilderPage</span>&gt; </span>&#123;</span><br><span class="line">  <span class="keyword">static</span> <span class="keyword">const</span> _itemCount = <span class="number">100</span>;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@override</span></span><br><span class="line">  Widget build(BuildContext context) &#123;</span><br><span class="line">    <span class="keyword">var</span> screenSize = MediaQuery.of(context).size;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> Scaffold(</span><br><span class="line">      appBar: AppBar(</span><br><span class="line">        title: <span class="keyword">const</span> Text(<span class="string">&#x27;ListViewBuilder List&#x27;</span>),</span><br><span class="line">      ),</span><br><span class="line">      body: Center(</span><br><span class="line">        child: SizedBox(</span><br><span class="line">          width: screenSize.width * <span class="number">0.6</span>,</span><br><span class="line">          height: screenSize.width * <span class="number">0.8</span>,</span><br><span class="line">          child: ListView.builder(</span><br><span class="line">            itemCount: _itemCount,</span><br><span class="line">            itemBuilder: (context, index) &#123;</span><br><span class="line">              <span class="keyword">return</span> <span class="keyword">const</span> Item();</span><br><span class="line">            &#125;,</span><br><span class="line">          ),</span><br><span class="line">        ),</span><br><span class="line">      ),</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<img src="/images/2022/20220525a/build_listviewbuilder.gif" alt="DevToolsのPerformance viewで確認" width="1200" height="442" loading="lazy">

<h2 id="メモリのプロファイリングデモ">メモリのプロファイリングデモ</h2><p>Flutter DevTools公式サイトのMemory view解説ページで紹介されるCase Studyを利用して、Memory viewの簡単な説明をしました。Network経由でサイズの大きな画像を多数連続して読み込んで表示するデモアプリのMemory使用を可視化しています。</p>
<ul>
<li>メモリ使用状況を時系列にグラフで確認できます。ユーザイベント、Heapメモリ、Nativeメモリ、Garbage collection (GC)などが表示されています。</li>
<li>メモリ使用のスパイク(40％以上)を検知してDevToolsが自動でSnapShotを取得（手動でも可能）して、その時のメモリ使用状況を分析できるようになっています。</li>
<li>以下ではImageCache PackageのObject数が急増していることを確認しています。</li>
</ul>
<img src="/images/2022/20220525a/memory_demo.gif" alt="メモリのプロファイリングデモ" width="1200" height="661" loading="lazy">

<p>補足：正確なPerformance測定は実機を用いた「Profile mode」での測定を前提としていますが、勉強会デモでは画面表示のためシミュレータのdebug modeでの確認としています。</p>
<h2 id="Q-A">Q&amp;A</h2><ul>
<li>Android studioでも同じようにPerformance測定できますか？<br>→ Android studioに、Flutter pluginをインストールすることでDevToolsも使用可能になります。参考：https://docs.flutter.dev/development/tools/devtools/android-studio</li>
<li>実業務での使用事例はありますか？<br>→ 現在のところは、開発プロセスに組み込んでの使用はしていませんが、課題発生時の原因究明に役立つと見込んでいます。</li>
</ul>
<h2 id="おわりに">おわりに</h2><p>PerformanceについてもFlutter stable releaseで毎回向上していて更新が多く注目度の高い開発ポイントだと分かります。DevToolsをFlutterアプリのPerformance課題解決と品質向上に役立てたいところです。</p>
<h2 id="参考">参考</h2><ul>
<li>Flutter DevTools公式サイトのMemory view解説ページ<ul>
<li>https://docs.flutter.dev/development/tools/devtools/memory</li>
</ul>
</li>
</ul>
]]></content>
    <summary type="html">Flutterアプリのperformance測定と基本的な実装ポイントについてご紹介します。1.Performanceについて、2.DevToolsについて...</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Flutter" scheme="https://future-architect.github.io/tags/Flutter/"/>
    <category term="プロファイリング" scheme="https://future-architect.github.io/tags/%E3%83%97%E3%83%AD%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AA%E3%83%B3%E3%82%B0/"/>
    <category term="性能検証" scheme="https://future-architect.github.io/tags/%E6%80%A7%E8%83%BD%E6%A4%9C%E8%A8%BC/"/>
    <category term="登壇レポート" scheme="https://future-architect.github.io/tags/%E7%99%BB%E5%A3%87%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
  </entry>
  <entry>
    <title>FigmaのOverlayに超入門してみた</title>
    <link href="https://future-architect.github.io/articles/20220418b/"/>
    <id>https://future-architect.github.io/articles/20220418b/</id>
    <published>2022-04-17T15:00:01.000Z</published>
    <updated>2022-04-17T15:00:01.000Z</updated>
    <author><name>真野隼記</name></author>
    <content type="html"><![CDATA[<p>春の入門連載 の1日目です。</p>
<h2 id="はじめに">はじめに</h2><p>Figma の入門記事です。Figmaとはデザインをブラウザ上で簡単に共同編集できることが特徴なUIデザインツールです。プレビュー実行すると作成したデザインがクリックなどのイベントで遷移させることができるので、WebサイトやPC・スマホアプリのワイヤーフレームやプロトタイプ作成で使われている場面をよく見ます。Figmaでプレゼンテーション用のスライドを作る人も噂では増えているようです。2022年3月16日に日本法人ができ国内でもさらに勢い増しそうな予感がします。</p>
<ul>
<li>世界でデザインコラボレーションツールをリードする「Figma」が日本へ本格進出｜Figma Japan株式会社のプレスリリース</li>
</ul>
<p>様々なデザインシステム<sup id="fnref:1">1</sup>もFigma上で公開されており、UIの生きた教材となっているようです（私はまだ眺めていてすごいなーくらいですが）</p>
<ul>
<li>勉強になったFigmaのデザインシステム8選｜東 莉緒／Rio Azuma｜note</li>
</ul>
<p>この記事ではFigmaを使って簡単なログインページを作ります。よくネットで見る参考情報では、入力フォームのキーボード入力などインタラクション性にかけるものが多かったので、オーバーレイを利用して動きをつけてアプリイメージを湧きやすくするように工夫します。</p>
<h2 id="Figmaことはじめ">Figmaことはじめ</h2><ul>
<li>公式サイトで、まずはログインします</li>
<li>New design fileを選択して開きます</li>
</ul>
<p>最初の使い方についてはサクッと始めるウェブデザイン【Figma】 がチュートリアルとして楽でした。また、Figmaの使い方はYouTube動画に多く説明されており、むしろそちらの方が情報が多い印象です。</p>
<p>よりよいデザインで作る場合は、既存のFigma Resources（Figmaで利用できる素材）をうまく活用し、むしろ乗っかっていく方がてっとり早いと思いますが、今回は使い方を学ぶということでなるべく手動で作る領域を多めにとる方針とします。</p>
<h2 id="つくるもの">つくるもの</h2><p>今回作るものですが、簡単なログインフォームを作ります。サンプルで引っかかるテキストの入力フォームがインタラクティブに動くものが無かったので、オーバーレイを利用し、スマートフォンのネイティブキーボードを表示させるインタラクションを作ります。少しでも動くとぐっとアプリのイメージが具体化されると思うからです。</p>
<img fetchpriority="high" src="/images/2022/20220418b/ログイン概念.png" alt="ログイン概念.png" width="1200" height="982">

<h2 id="流れ">流れ</h2><p>New design fileで開くと、バナーからフレームを選択します。</p>
<img src="/images/2022/20220418b/フレーム選択.png" alt="フレーム選択.png" width="559" height="63" loading="lazy">

<p>サイドバーにどういったテンプレートを作成するかプルダウンが表示されるので、iPhone 13 Pro Maxを選択します。</p>
<img src="/images/2022/20220418b/モバイルを選択.png" alt="モバイルを選択.png" width="494" height="266" loading="lazy">

<p>オブジェクト挿入し、入力ボックスを作っていきます。</p>
<img src="/images/2022/20220418b/オブジェクト挿入.png" alt="オブジェクト挿入.png" width="573" height="180" loading="lazy">

<p>Cornar RADIUSで少し端っこを丸くすると入力フォームぽくなります。</p>
<img src="/images/2022/20220418b/角丸.png" alt="角丸.png" width="356" height="271" loading="lazy">

<p>テキストでラベル・入力のダミー値を設定します。固定値です。</p>
<img src="/images/2022/20220418b/テキスト入力.png" alt="テキスト入力.png" width="592" height="76" loading="lazy">

<p>ログインボタンはCornar RADIUSを強めにするして色を塗りつぶすとそれっぽく見えます。同様にトップページも作っておき、2ページ並べて全体を見ると次のような状態を作ります。</p>
<img src="/images/2022/20220418b/全体モック.png" alt="全体モック.png" width="1200" height="599" loading="lazy">

<h2 id="キーボードを追加">キーボードを追加</h2><p>デフォルトだとキーボードを表示するような仕組みが無いため、素材をもとに作り込む必要があります。Figma上からCommunityが上げている素材を検索します。keyboardなどで検索すると、例えば以下のようなページが見つかると思います。</p>
<ul>
<li>https://www.figma.com/community/file/1029525540844129321</li>
</ul>
<p>Duplicate ボタンを押すと、別タブでFigma編集画面が表示されます。</p>
<img src="/images/2022/20220418b/複製.png" alt="複製.png" width="1143" height="538" loading="lazy">

<p>利用したいリソースをコピーして、編集中のデザインファイルに貼り付けます。</p>
<img src="/images/2022/20220418b/リソースをコピー.png" alt="リソースをコピー.png" width="1161" height="400" loading="lazy">

<p>横幅を調整して合わせます（少しキーボタンが崩れますが、調整は割愛）。</p>
<img src="/images/2022/20220418b/キーボード貼り付け.png" alt="キーボード貼り付け.png" width="997" height="380" loading="lazy">

<p>ここで入力フォームをクリック時にPrototype+Overrayでキーボードが立ち上がるようにします。</p>
<p>入力ボックスをクリック、PrototypeからInteractionsでOpen overlayを選択。</p>
<img src="/images/2022/20220418b/prototype.png" alt="prototype.png" width="926" height="506" loading="lazy">

<p>先程追加した「キーボード」を選択します。Overlayでは、Bottom centerで被さる位置を画面下部にし、「Close when clicking outside」で外すようにします。AnimationではMove inを選び、進行方向を「↑」をクリックします。</p>
<img src="/images/2022/20220418b/Overlay設定.png" alt="Overlay設定.png" width="721" height="705" loading="lazy">

<p>ログインボタンの遷移はもっとシンプルで、On tap時に Navigate to でトップページを選択します。</p>
<img src="/images/2022/20220418b/ログインボタンの遷移.png" alt="ログインボタンの遷移.png" width="791" height="439" loading="lazy">

<h2 id="デモ">デモ</h2><p>ナビゲーションバーの「▷」でプレゼンテーションモードで再生できます。</p>
<p>入力フォームを選択すると、キーボードがぬっと立ち上がるのがわかると思います。キーボード入力はできませんが、このレベルでもアプリの導線があたえる印象がガラッと変わってくると思います。</p>
<img src="/images/2022/20220418b/figma_demo.gif" alt="figma_demo.gif" width="1200" height="682" loading="lazy">

<h2 id="まとめ">まとめ</h2><p>Figmaのオーバーレイの超入門記事でした。コミュニティのみなさんが共有してくださっている素材をうまく組み合わせるとちょっとした時間でワイヤーフレーム・プロトタイプの品質をぐっと向上させることができます。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">https://www.concentinc.jp/design_research/2021/04/designsystem/</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">Figmaを使って簡単なログインページを作ります。よくネットで見る参考情報では、入力フォームのキーボード入力などインタラクション性にかけるものが多かったので、オーバーレイを利用して動きをつけてアプリイメージを湧きやすくするように工夫します。</summary>
    <category term="Mobile" scheme="https://future-architect.github.io/categories/Mobile/"/>
    <category term="Figma" scheme="https://future-architect.github.io/tags/Figma/"/>
    <category term="入門" scheme="https://future-architect.github.io/tags/%E5%85%A5%E9%96%80/"/>
  </entry>
</feed>
