Ionicのチュートリアルをやってみる (1)

[Ionicの公式ドキュメント](https://ionicframework.com/docs/)にあるチュートリアルを試してみる（動作確認したのは2024/06末）。

手順を試した記録と、OutSystemsとの絡みで気になる点をメモしていく。

まず、最初のステップとして、セットアップ、アプリケーション作成、写真撮影と一覧表示機能部分までをみていく（写真撮影には、Capacitor利用）。その他は後日。モバイルアプリケーションとして動作させるのはそちらに含まれ、今回分ではPWAとして動作させる。

今回見たドキュメントは、
- [Getting Started](https://ionicframework.com/docs)の下にあるもの
- [React向けのBuild your first app](https://ionicframework.com/docs/react/your-first-app)
- [React向けのTaking Photos](https://ionicframework.com/docs/react/your-first-app/taking-photos)

# 確認環境
MacBook Air (M2, 2022)
node (v22.0.0)
npm (10.5.1)

# 感想（主にOutSystemsとの関係で）とメモ
## [Overview](https://ionicframework.com/docs/)から
> Ionic uses Capacitor (or Cordova) to deploy natively, or runs in the browser as a Progressive Web App.

動作させるのに
- CapacitorかCordovaを使ってネイティブなアプリとしてデプロイする
- PWAとして動作させる
の２通りある。

OutSystemsのモバイルアプリケーションは、ネイティブ機能に触るときに、Cordovaを使う。Ionicの場合はそのレイヤとして、CapacitorとCordovaが選べるという感じか。

> Simplicity
Ionic is built with simplicity in mind, so that creating apps is enjoyable, easy to learn, and accessible to just about anyone with web development skills.

Web開発スキルを持つ人なら誰でもモバイル開発できるようにする。この点でも確かに、OutSystemsとは親和性高そう。
## [Packages and CDN](https://ionicframework.com/docs/intro/cdn)から
Ionicは各種フロントエンドフレームワークとの組み合わせの他、VanillaのJavaScriptでも使える。よって、codepenとかで利用できるらしい。

IonicのアイコンはIoniconと呼び、独立して利用できる。ライセンスはMIT。使うには、bodyの閉じタグ直前に参照をおけば良い。
bodyの直前なのは、動作に必要ないリソースだから、ページ表示に影響しないようにかな？

## 感想
確かに、OutSystemsが（会社の方を）買収したのもわかるな、と思った。
- OutSystemsと同じく、Webの開発技術を使ってモバイルアプリケーションを作るスタイル
- OutSystemsのモバイルアプリケーションでは、ネイティブ機能を操作するプラグイン開発にCordovaを使っている。Ionicなら、このレイヤはCapacitorとCordovaを選べる
- OutSystemsで使っているReactとも問題なく共存できる（とドキュメントに書かれている）
- 今回見たチュートリアルの範囲だと、Ionicによる開発は、OutSystems開発のクライアント部分と似た感覚でできる部分が多そう

# セットアップ
## 前提ソフトウェア 
[Environment Setup](https://ionicframework.com/docs/intro/environment)に記載あり。

nodeとnpmが必須なのでインストールする。
何かの機会でインストールしていたようで入っていたのでこれを使う。
```
junji@Watanabes-MacBook-Air Ionic % node --version
v22.0.0
junji@Watanabes-MacBook-Air Ionic % npm --version
10.5.1
```

また、バージョン管理としてgit、コードエディタとしてVSCodeが推奨されている。
なお、VSCode向けにIonicのExtensionがある（以下の手順では、このExtensionは使わない）。

[Ionic VS Code Extension](https://ionicframework.com/docs/intro/vscode-extension)

以下のExtensionを入れる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/26e91550-2c79-a4d7-17aa-1bbe258f108c.png)

Activity Barの最下部にIonicのアイコンが表示される。ExtensionをDisableにすれば、このアイコンは非表示になる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/3a9b4afd-836a-0b7c-6108-e4c17b8c17da.png)

## IonicのCLI
[CLI　（Command Line Interface) Installation](https://ionicframework.com/docs/intro/cli)に手順記載がある。
CLIを使って、アプリケーション作成・リリース向けのビルド・開発用サーバの起動等をする。

OSのターミナルを開いて、「npm install -g @ionic/cli」を入力してインストール。以降でコマンドを入力しているときも同様にOSのターミナル（たまにVSCodeのTerminal)を利用している。

```
junji@Watanabes-MacBook-Air Ionic % npm install -g @ionic/cli 
npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm WARN deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm WARN deprecated superagent@8.1.2: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net

added 204 packages in 10s

33 packages are looking for funding
  run `npm fund` for details
junji@Watanabes-MacBook-Air Ionic % npm ls -g @ionic/cli
/opt/homebrew/lib
└── @ionic/cli@7.2.0
```

使われているパッケージでdeprecatedの警告が出ている。
このくらい出るものなのか？
Geminiに聞いてみたら、これらのdeprecatedなパッケージは、今インストールした時に入ったもののようだが。

# アプリケーション作成
[Your First Ionic App: React](https://ionicframework.com/docs/react/your-first-app)の手順を追ってみる。
まずは、以下のnpm packageをインストールする（ドキュメントでは@ionic/cliもインストールするコマンドになっているが、ここでは前の手順でインストール済みなのでスキップ）。
- native-run: ネイティブアプリとして端末やエミュレーターで動作させる
- cordova-res: ネイティブアプリのアイコンとsplash screen (OutSystemsにもあるやつ。アプリ起動時の最初のローディング中みたいな画面）
```
junji@Watanabes-MacBook-Air Ionic % npm install -g native-run cordova-res

added 108 packages in 28s

13 packages are looking for funding
  run `npm fund` for details
```

「ionic start」コマンドで「photo-gallery」アプリをタブテンプレートで作成する。OutSystemsとの絡みで見ていくので、（多分あんまり変わらないと思うが、OutSystemsは内部でReactを使っているので）「type=react」オプションをつけている。

試してみたら結構かかったので、ionic startの前後をdateで挟んで大まかな経過時間を見た。だいたい1分。途中でionicアカウントを作るか聞いてくる。とりあえずnにしておいた。

```
junji@Watanabes-MacBook-Air Ionic % date
2024年 6月29日 土曜日 15時26分38秒 JST
junji@Watanabes-MacBook-Air Ionic % ionic start photo-gallery tabs --type=react --capacitor
✔ Preparing directory ./photo-gallery in 486.63μs
✔ Downloading and extracting tabs starter in 265.25ms
> ionic integrations enable capacitor --quiet -- photo-gallery io.ionic.starter
> npm i --save -E @capacitor/core@latest
npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm WARN deprecated @humanwhocodes/config-array@0.11.14: Use @eslint/config-array instead
npm WARN deprecated abab@2.0.6: Use your platform's native atob() and btoa() methods instead
npm WARN deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm WARN deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
npm WARN deprecated domexception@4.0.0: Use your platform's native DOMException instead

added 691 packages, and audited 692 packages in 38s

154 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
> npm i -D -E @capacitor/cli@latest

added 64 packages, and audited 756 packages in 5s

160 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
> npm i --save -E @capacitor/haptics @capacitor/app @capacitor/keyboard @capacitor/status-bar

added 4 packages, and audited 760 packages in 7s

160 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
> capacitor init photo-gallery io.ionic.starter --web-dir dist
✔ Creating capacitor.config.ts in /Users/junji/Ionic/photo-gallery in 1.25ms
[success] capacitor.config.ts created!

Next steps: 
https://capacitorjs.com/docs/getting-started#where-to-go-next
[OK] Integration capacitor added!

Installing dependencies may take several minutes.

  ──────────────────────────────────────────────────────────────────────────────

         Ionic Advisory, tailored solutions and expert services by Ionic

                             Go to market faster 🏆
                    Real-time troubleshooting and guidance 💁
        Custom training, best practices, code and architecture reviews 🔎
      Customized strategies for every phase of the development lifecycle 🔮

                        👉  https://ion.link/advisory  👈

  ──────────────────────────────────────────────────────────────────────────────


> npm i

up to date, audited 760 packages in 581ms

160 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
> git init
Initialized empty Git repository in /Users/junji/Ionic/photo-gallery/.git/

Join the Ionic Community! 💙

Connect with millions of developers on the Ionic Forum and get access to live
events, news updates, and more.

? Create free Ionic account? No
> git add -A
> git commit -m "Initial commit" --no-gpg-sign
[main (root-commit) 050d2f0] Initial commit
 Committer: Watanabe Junji <junji@Watanabes-MacBook-Air.local>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

    git config --global --edit

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 33 files changed, 10747 insertions(+)
 create mode 100644 .browserslistrc
 create mode 100644 .eslintrc.js
 create mode 100644 .gitignore
 create mode 100644 .vscode/extensions.json
 create mode 100644 capacitor.config.ts
 create mode 100644 cypress.config.ts
 create mode 100644 cypress/e2e/test.cy.ts
 create mode 100644 cypress/fixtures/example.json
 create mode 100644 cypress/support/commands.ts
 create mode 100644 cypress/support/e2e.ts
 create mode 100644 index.html
 create mode 100644 ionic.config.json
 create mode 100644 package-lock.json
 create mode 100644 package.json
 create mode 100644 public/favicon.png
 create mode 100644 public/manifest.json
 create mode 100644 src/App.test.tsx
 create mode 100644 src/App.tsx
 create mode 100644 src/components/ExploreContainer.css
 create mode 100644 src/components/ExploreContainer.tsx
 create mode 100644 src/main.tsx
 create mode 100644 src/pages/Tab1.css
 create mode 100644 src/pages/Tab1.tsx
 create mode 100644 src/pages/Tab2.css
 create mode 100644 src/pages/Tab2.tsx
 create mode 100644 src/pages/Tab3.css
 create mode 100644 src/pages/Tab3.tsx
 create mode 100644 src/setupTests.ts
 create mode 100644 src/theme/variables.css
 create mode 100644 src/vite-env.d.ts
 create mode 100644 tsconfig.json
 create mode 100644 tsconfig.node.json
 create mode 100644 vite.config.ts

Your Ionic app is ready! Follow these next steps:

- Go to your new project: cd ./photo-gallery
- Run ionic serve within the app directory to see your app in the browser
- Run ionic capacitor add to add a native iOS or Android project using Capacitor
- Generate your app icon and splash screens using cordova-res --skip-config
--copy
- Explore the Ionic docs for components, tutorials, and more:
https://ion.link/docs
- Building an enterprise app? Ionic has Enterprise Support and Features:
https://ion.link/enterprise-edition
junji@Watanabes-MacBook-Air Ionic % date
2024年 6月29日 土曜日 15時27分38秒 JST
```
実行結果を見ると、内部でgitにコミットしている。gitインストールしていなかったらどうなるかは気になるが、一旦おいておく。

作成された「photo-gallery」アプリのフォルダ内にCapacitorのプラグインをインストール（フォルダ内なので-gがついていない）。
「npm install @capacitor/camera @capacitor/preferences @capacitor/filesystem」
「npm install @ionic/pwa-elements」
```
junji@Watanabes-MacBook-Air Ionic % cd photo-gallery
junji@Watanabes-MacBook-Air photo-gallery % npm install @capacitor/camera @capacitor/preferences @capacitor/filesystem

added 3 packages, and audited 763 packages in 5s

160 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
junji@Watanabes-MacBook-Air photo-gallery % npm install @ionic/pwa-elements

added 1 package, and audited 764 packages in 1s

160 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
```

以下でハイライトした行は、CapacitorのPluginにWebベースでアクセスするための追加行。
拡張子.tsxなのでTypeScriptか。
``` diff_tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
+import { defineCustomElements } from '@ionic/pwa-elements/loader';

+// Call the element loader before the render call
+defineCustomElements(window);

const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```

動作確認。アプリのディレクトリ内で「ionic serve」を入力すると開発用サーバーが立ち上がり、作成した画面が開く。なお、ionic serveを起動中にソースコードを編集して保存すると、自動で動作中のアプリに反映される。
```
junji@Watanabes-MacBook-Air photo-gallery % ionic serve
> vite --host=localhost --port=8100
[vite]   ➜  Local:   http://localhost:8100/
[vite]   ➜  press h + enter to show help

[INFO] Development server running!
       
       Local: http://localhost:8100
       
       Use Ctrl+C to quit this process

[INFO] Browser window opened to http://localhost:8100!
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/75dc00f0-f9e2-ef76-eb00-e25e23d8ad3f.png)

画面下部に3つのタブがあるが、その真ん中（Tab2）を編集し、カメラを起動するためのボタンを配置する。
アプリ内のsrc/pages/Tabs2.tsxが真ん中のタブに対応する画面定義ファイルなのでこれを編集。
変更箇所（ハイライトしている部分）：
- IonTitle： ページタイトル変更
- Import追加：　チュートリアルで指定されているimportには元からアプリに含まれているのもあったので、一部省略している
- IonFab追加：　Floating Action Button。この後、このボタンをクリックするとカメラが起動するようにしていく。OutSystemsなら[Floating Actions](https://outsystemsui.outsystems.com/StyleGuidePreview/PatternDetail?MenuCategoryId=3&MenuSubCategorId=19)かな
``` diff_tsx:src/pages/Tabs2.tsx
 import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
 import ExploreContainer from '../components/ExploreContainer';
 import './Tab2.css';
 
+import { camera, trash, close } from 'ionicons/icons';
+import {
+  IonFab,
+  IonFabButton,
+  IonIcon,
+  IonGrid,
+  IonRow,
+  IonCol,
+  IonImg,
+  IonActionSheet,
+} from '@ionic/react';
+
 const Tab2: React.FC = () => {
   return (
     <IonPage>
       <IonHeader>
         <IonToolbar>
-          <IonTitle>Tab 2</IonTitle>
+          <IonTitle>Photo Gallery</IonTitle>
         </IonToolbar>
       </IonHeader>
       <IonContent fullscreen>
         <IonHeader collapse="condense">
           <IonToolbar>
             <IonTitle size="large">Tab 2</IonTitle>
           </IonToolbar>
         </IonHeader>
         <ExploreContainer name="Tab 2 page" />
+        <IonContent>
+          <IonFab vertical="bottom" horizontal="center" slot="fixed">
+            <IonFabButton onClick={() => takePhoto()}>
+              <IonIcon icon={camera}></IonIcon>
+            </IonFabButton>
+          </IonFab>
+        </IonContent>
       </IonContent>
     </IonPage>
   );
 };
 
 export default Tab2;
 ```

Reactアプリ内でレイアウトを決めるらしい、src/App.tsxを編集し、画面下部に表示する真ん中のタブのアイコンとテキストを変更する。
``` tsx:src/App.tsx
-import { ellipse, square, triangle } from 'ionicons/icons';
+import { images, square, triangle } from 'ionicons/icons';
(中略)
           <IonTabButton tab="tab2" href="/tab2">
-            <IonIcon aria-hidden="true" icon={ellipse} />
-            <IonLabel>Tab 2</IonLabel>
+            <IonIcon aria-hidden="true" icon={images} />
+            <IonLabel>Photos</IonLabel>
           </IonTabButton>
```

この時点の画面。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/b79c83e1-d62d-720a-ad74-bd5af0e3860f.png)

# 写真撮影と一覧表示機能
[Taking Photos with the Camera](https://ionicframework.com/docs/react/your-first-app/taking-photos)の手順。
ここでやるのは、
- 上の手順で追加したIonFabButtonクリック時にカメラを起動する
- 撮影後に画像を受け取ってタブ内に一覧表示する

カメラ起動ロジックを追加して最初のテストでは、IonFabButtonクリック直後にクラッシュした。ionic serveしなおしたらカメラ起動した（OSがカメラ利用の許可していいか聞いてくる）。

UI操作時の処理はhookという仕組みで作成する（OutSystemsで言えば、UIのEventsに紐付けるScreenやBlock下のClient Action）ので、srcの下にhooksディレクトリを作成する。
hooksディレクトリ下にusePhotoGallery.tsを作成。

``` tsx:src/hooks/usePhotoGallery.ts
import { useState, useEffect } from 'react';
import { isPlatform } from '@ionic/react';

import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Preferences } from '@capacitor/preferences';
import { Capacitor } from '@capacitor/core';

export function usePhotoGallery() {
    const [photos, setPhotos] = useState<UserPhoto[]>([]);
    const takePhoto = async () => {
        const photo = await Camera.getPhoto({
            resultType: CameraResultType.Uri,
            source: CameraSource.Camera,
            quality: 100,
        });
        const fileName = Date.now() + '.jpeg';
        const newPhotos = [
            {
                filepath: fileName,
                webviewPath: photo.webPath,
            },
            ...photos,
        ];
        setPhotos(newPhotos);
    };

    return {
        photos,
        takePhoto,
    };
}

export interface UserPhoto {
    filepath: string;
    webviewPath?: string;
}
```

作成したusePhotoCalleryをimportし、IonFabButtonクリック時に呼ばれているtakePhotoを受け取る。
``` diff_tsx:src/pages/Tabs2.tsx
+import { usePhotoGallery } from '../hooks/usePhotoGallery';
+
 const Tab2: React.FC = () => {
+  const { photos, takePhoto } = usePhotoGallery();
   return (
     <IonPage>
```

手元にあった適当なメモ帳を撮影した
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/dbb46b38-a42a-89ef-1dc5-efd33a09ffd4.png)

写真を2回撮って一覧表示した様子。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/185dabf0-bdab-bfa6-b1f7-8b4bdd362655.png)