初めてReactNative+ExpoでiOSアプリ開発をして躓いたところ

# はじめに
私、現在は大学4年生(いわゆるFランですが・・・)、内定承諾済み、卒業研究もやっています(なんと選択科目) がまだ佳境ではなかった、ということで知人経由でアプリ制作の依頼を受けて何も知らない状態からアプリ制作を始めてみました。

RN+Expo、iPad向けの社内用アプリです。
Flutterも考えましたが、もともとWebコーディング専攻であることもありスタイリングがCSSライクなRNにしました。
Webアプリにしとけよってところですが、iPadで使いたいこととサーバーを用意、管理するのが面倒という要望があり、アプリ制作に手を出してみたかった気持ちを優先しました。

ひとまず、備忘録兼後の世代の人が検索して役に立つと良いなという淡い陰謀のもと、私が躓いた箇所をメモのように残しておこうと思います。

# 開発環境
期間：2023/04~2023/06
Thinkbook 13thgen Windows10 Pro 
(後々iMac 2017 21.7inchを購入)
iPad Air 第6世代
node v18.14.2
npm v9.5.0
expo v49.0.3
RN v0.72.3
React v18.2.0

# 本題
### 実機でデバッグできない
Windows+iOS端末で発生?iMacでは発生しなかった
実行コマンドに--tunnelを付ける
```diff
- npx expo start
+ npx expo start --tunnel
```

### propsで渡ってきた関数をonPressで使いたいけどなんかループするエラーが起きる
受け取った先で関数に入れる。
```diff_jsx
- onPress = {props.func}
+ onPress = {()=>props.func()}
```

### Stack.Screenでprops渡ししたい
Stack.Screenの開きタグと閉じタグを作り、関数にコンポーネントを仕舞って子要素にする。
```diff_jsx
- <Stack.Screen name="index" component="Index" />
+ <Stack.Screen>
+     { () => (
+        <Index setTitle={this.saveResultTitle()} />
+     )}
+ </Stack.Screen>
```

### onPressで使いたい関数をpropsで渡せない
onPressで使うpropsはすべてonPressで渡す。
```diff_jsx
- <Index touch={()=>console.log("fire")} />
+ <Index onPress={()=>console.log("fire")} />
```
```jsx
function Index(props) {
    return (
        <View>
            <Button onPress={()=>props.onPress()}
        </View>
}
```

### justifyContent, justifySelfが効かない
プロパティ値は"space-between"、"flex-start"、"flex-end"と記述する。
```jsx
const style = StyleSheet.create({
    index: {
        justifyContent: "space-between",
        justifyContent: "flex-start",
        justifyContent: "flex-end",

        justifySelf: "space-between",
        justifySelf: "flex-start",
        justifyself: "flex-end",
    },
})
```

### Imageコンポーネントで画像が表示できない
widthとheightが必須。
画像形式はpngかjpg(jpeg?)のみ。
```diff_jsx
<Image 
    style={{
        width: 47,
        height: 47,
    }}
-    source={require("./assets/setting.svg")}
+    source={require("./assets/setting.png")}
/>
```

### ScrollViewでjustifyContentやalignItemsがエラーになる
代わりにcontentContainerStyleを使う。
```diff_jsx
<ScrollView
-    style={{
+    contentContainerStyle={{
        justifyContent: "center",
        alignItems: "center",
        height: "100%",
    }}
>
```

### react-native-html-to-pdfでconvertが見つからないと言われる
Expoを使っているとReactNativeのライブラリのうちExpoに対応しているものしか使えない。
類似ライブラリで代替する。

### Possible Ungandled Promise Rejectが発生する
宣言していない変数、importしていないライブラリやコンポーネントなどがあると発生する。
コピペコードに注意。

### Expo printでエラー
類似ライブラリでも引数や返り値が配列、オブジェクト、JSON、Stringなど異なることが多い。
使おうと思ったライブラリがExpo非対応だったときは特に公式ドキュメントで要チェック。

### AsyncStorageで全権取得ができない、returnしたObjectがバグる
async関数の外でreturnさせた値はPromiseでラップされてしまうので通常の値のように使えない。
.then()メソッドで処理しないとオブジェクトとして受け取れない。
(ReactNativeというより単にJavaScriptのミス)
```JavaScript
const getAll = async () => {
    try {
        const keys = await AsyncStorage.getAllkeys();
        const value = await AsyncStorage.multiGet(keys);
        return value;
    } catch (e) {
        alert(e);
}
```
```diff_javascript
- const value = getAll();
+ getAll().then((res)=>{ this.setState((value: res))}
```

### マウント時にAsyncStorageから値を取得するコンポーネントにおいて、Objectのプロパティが参照できないエラーが発生する
例えば、AsyncStorageに
```javascript
{obj: "dosent exist"}
```
が格納されていたとして、取得した値を定数valueに格納するとする。
このとき、
```javascript
return (
    <View>
        <Text>{value.obj}</Text>
    </View>
)
```
としてしまうと、
描画→値取得→再描画(setState)
の順を追うので最初の描画の時点でundefinedに対して存在しないプロパティを参照してエラーになる。
```diff_javascript
return (
    <View>
-        <Text>{value.obj}</Text>
+        <Text>{!value ? "" : value.obj}</Text>
    </View>
)
```
このように参考演算子などを使って存在してから参照するようにする。

### デバッグにはalertよりconsole.logが便利
alertはデバッグ中PrintStringのようにデバイス上で値を確認するために使うこともあるが、WebViewでなくてもconsole.logを使えばPowerShellなどに値を出力できる。

# 最後に
次回、~eas buildで躓いたところ編~でお会いしましょう。


# 最後ではなくなった
build中のエラーの解決方法はすっかり忘れてしまいました。
cocoapodsとfastlaneの設定不足だったので多分皆さんも解決できるでしょう。

### build出来たのにクラッシュする
さて、buildが通って.ipaファイルを作ることができたのに実機にインストールするとアプリが使えません。
起動した瞬間にクラッシュしてしまうのです。
エラーログとコンソールを確認してみました。
(→デバイスログとコンソールの開き方、.ipaファイルを実機にインストールする方法は最後に紹介します。)

デバイスログからProcessがアプリ名、TypeがCrashになっているもののうち最新のログを見てみます。
```
Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Termination Reason: SIGNAL 6 Abort trap: 6
Terminating Process: /* アプリ名 */ [2680]
```
終了理由がSIGNAL 6 Abort trap: 6とのこと。
しかし、この文言で検索しても同じ状況っぽいものは出てきません。

次にコンソールで確認しました。
アプリが起動してからクラッシュするまでのメッセージを確認します。
コンソールではデバイス内で起きていること全てのメッセージが出力されています。
列名「プロセス」をクリックするとプロセスごとにソートしてくれるので、アプリ名を探してメッセージを確認していきます。
```
Unhandled JS Exception: Invariant Violation: requireNativeComponent: "RNCSafeAreaProvider" was not found in the UIManager.

This error is located at: 
/* 以下略 */
```
直接原因を教えてくれているっぽいのがありました。
「RNSafeAreaProvider」が見つからないとのことです。
調べてみると原因が特定できました。
ReactNavigationの依存パッケージであるreact-native-safe-area-contextがないことが原因でした。
ReactNavigationドキュメント→https://reactnavigation.org/docs/getting-started/
ドキュメントにも書いてありますが、情報が古いブログを読んでいたこととドキュメントを確認していなかったため見逃してしまいました。

ちなみに、react-native-safe-area-contextはデバイスのノッチや通知エリアを避けるためのライブラリです。


とはいえ幸いあまりコードは変わらず。
react-native-safe-area-providerをインストールしたら
・1ページをまるごと構成するComponentをSafeAreaProviderでラップする
・SafeAreaProviderの直下をViewからSafeAreaViewに変更する
で済みました。

その後はビルドコマンドを実行して.ipaファイルを書き出し実機にインストールして無事起動することが確認できました。

### アプリの挙動がおかしい
そもそもこのアプリは質問に回答すると最終的な金額を算出してくれるというアキネーター式の金額計算ツールだったのですが、金額の設定を設定画面から変更するとあり得ないほどの高額になってしまうという問題が発生しました。

理由は簡単。
初期設定→数値型
ユーザが書き換えた設定→文字列型
大元となるプランを選択し追加で質問に答えていくことで金額が加算されていく、というやり方です。
そのため大元のプランの金額設定が文字列になってしまうと、50万円が"50"万円に、5万円加算されると55万円ではなく"505"万円になってしまいます。

ケアレスミスかも怪しい。

# おまけ：デバイスログとコンソールの開き方
多分誰かがネット上で教えてくれるでしょうが、同じ記事にまとまっていた方が便利ですよね。

1.実機とMacを接続します。
2.Xcodeを開きます。
3.メニューバーの「Window」から「Devices and Simulators」を開きます。(Shift+Command+2)
4.Devicesタブから接続している実機を選択します。
5.右エリア上部にある「View Device Logs」でデバイスログを、「Open Console」でコンソールを開くことができます。

# おまけ：.ipaファイルを実機にインストールする方法
こちらは先に解説している方がいらっしゃいましたが、その方自身が検索しても出てこなかったため備忘録として残されていました。
情報源は多い方がいいでしょうってことで書いておきます。

1.実機とMacを接続しXcodeの「Devices and Simulators」ウィンドウを開きます。
2.Devicesタブから接続している実機を選択します。
3.右エリアの「INSTALLED APPS」からインストールできます。
![スクリーンショット 2024-03-18 17.47.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3438525/6ca13039-637b-e542-ffb8-be44b218880d.png)
下の+ボタンでファイルピッカーが開きます。
.ipaファイルを選択するとインストールが始まります。
インストールされたアプリはここに一覧表示されます。

一覧からアプリを選択して下の-ボタンでアンインストールできます。
もちろん、実機側からアンインストールすることもできます。