tag:crieit.net,2005:https://crieit.net/tags/ReactNative/feed 「ReactNative」の記事 - Crieit Crieitでタグ「ReactNative」に投稿された最近の記事 2021-10-18T09:38:53+09:00 https://crieit.net/tags/ReactNative/feed tag:crieit.net,2005:PublicArticle/17703 2021-10-12T09:52:01+09:00 2021-10-18T09:38:53+09:00 https://crieit.net/posts/React-Navigation-Typescript React NavigationをTypescriptと一緒に使う際につまづいたところ <p>最近業務でReact Nativeを書いているのですが、ナビゲーションのライブラリとして<br /> React NavigationをTypescriptと一緒に使う際に色々大変だったので(主にドキュメントを読んでいないせい)<br /> まとめておこうと思いこの記事を書いています。質問や気になる点があれば<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kmgk21444557">@kmgk21444557</a>までご連絡ください。</p> <p>アプリの作成、実行にはExpoを使っています。</p> <p>サンプルアプリのリポジトリ:<a target="_blank" rel="nofollow noopener" href="https://github.com/kmgk/ReactNavigationExample">https://github.com/kmgk/ReactNavigationExample</a></p> <h2 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h2> <pre><code>node: v16.10.0 yarn: 1.22.4 expo-cli: 4.12.1 OS: android (Pixel 3a XL API R) </code></pre> <p><code>package.json</code></p> <pre><code class="json">{ "dependencies": { "@react-navigation/bottom-tabs": "^6.0.7", "@react-navigation/native-stack": "^6.2.2", "expo": "~42.0.1", "expo-status-bar": "~1.0.4", "react": "16.13.1", "react-dom": "16.13.1", "react-native": "https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz", "react-native-safe-area-context": "3.2.0", "react-native-screens": "~3.4.0", "react-native-web": "~0.13.12" }, "devDependencies": { "@babel/core": "^7.9.0", "@react-navigation/native": "^6.0.4", "@types/react": "~16.9.35", "@types/react-native": "~0.63.2", "typescript": "~4.0.0" }, } </code></pre> <h2 id="テスト用アプリの作成"><a href="#%E3%83%86%E3%82%B9%E3%83%88%E7%94%A8%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E4%BD%9C%E6%88%90">テスト用アプリの作成</a></h2> <p>expo-cliを用いてアプリを作成します。テンプレートは<code>blank (Typescript)</code>を選択しました。</p> <pre><code>$ expo init ReactNavigationExample </code></pre> <p>必要なパッケージをインストール</p> <pre><code>yarn add @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs expo install react-native-screens react-native-safe-area-context react-native-pager-view </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/hello-react-navigation">Hello React Navigation</a>を参考に、<br /> <code>HomeScreen</code>と<code>HogeScreen</code>の2画面作成し、<code>createNativeStackNavigator</code>でナビゲーションを作成します。</p> <p><code>App.tsx</code></p> <pre><code class="tsx">import { NavigationContainer, useNavigation } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; import { Button, StyleSheet, Text, View } from 'react-native'; const HomeStack = createNativeStackNavigator(); export default function App() { return ( <NavigationContainer> <HomeStack.Navigator initialRouteName="Home"> <HomeStack.Screen name="Home" component={HomeScreen} /> <HomeStack.Screen name="Hoge" component={HogeScreen} /> </HomeStack.Navigator> </NavigationContainer> ); } const HomeScreen: React.FC = () => { const navigation = useNavigation() return ( <View style={styles.container}> <Text>HomeScreen</Text> <Button title="Go To HogeScreen" onPress={() => navigation.navigate('Hoge')} /> </View> ) } const HogeScreen: React.FC = () => { return ( <View style={styles.container}> <Text>HogeScreen</Text> </View> ) } const styles = StyleSheet.create({ ... }); </code></pre> <h2><code>navigate('Hoge')</code>でエラーが出る</h2> <p>まずここでエラーが出ます。<code>onPress={() => navigation.navigate('Hoge')}</code>のnavigateの引数である文字列Hogeの型が正しくないようです。</p> <pre><code>型 'string' の引数を型 '{ key: string; params?: undefined; merge?: boolean | undefined; } | { name: never; key?: string | undefined; params: never; merge?: boolean | undefined; }' のパラメーターに割り当てることはできません </code></pre> <p>'Hoge'が割り当てられる予定のnameパラメータの型がneverになっています。なんで。</p> <p>ここで公式に<a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/typescript">Type checking with TypeScript</a>というページがあることに気が付きます。</p> <blockquote> <p>To type check our route name and params, the first thing we need to do is to create an object type with mappings for route name to the params of the route. For example, say we have a route called Profile in our root navigator which should have a param userId:<br /> とあるのでとりあえずルートをまとめたオブジェクト<code>RootStackParamList</code>を作成します。</p> </blockquote> <pre><code class="tsx">type RootStackParamList = { Home: undefined; Hoge: undefined; } </code></pre> <p>型にundefinedを指定するとナビゲーションからパラメータを与えられません。逆にパラメータが欲しい場合はそのパラメータのマッピングを指定する必要があります。</p> <p>これを<code>createNativeStackNavigator</code>に指定します。</p> <pre><code>const HomeStack = createNativeStackNavigator<RootStackParamList>(); </code></pre> <p>こうすることでNavigator内部のScreenでnameが制限されます。<br /> 定義したルート名しか使えないのでtypoの心配もなくなりました。</p> <p><img src="https://github.com/kmgk/blog/blob/main/static/images/stack-screen-strict-name.png?raw=true" alt="Navigator内部のScreenでnameが制限" /></p> <p>ただ、まだ<code>navigate('Hoge')</code>のエラーは出たままです。先程の記事の<a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/typescript#type-checking-screens">Type checking screens</a>を見ると</p> <blockquote> <p>To type check our screens, we need to annotate the navigation prop and the route prop received by a screen. The navigator packages in React Navigation export a generic types to define types for both the navigation and route props from the corresponding navigator.<br /> The type takes 2 generics, the param list object we defined earlier, and the name of the current route. This allows us to type check route names and params which you're navigating using navigate, push etc. The name of the current route is necessary to type check the params in route.params and when you call setParams.<br /> また、<code>useNavigation</code>をよく見てみると</p> </blockquote> <pre><code class="tsx">useNavigation<NavigationProp<ReactNavigation.RootParamList, ... </code></pre> <p>とあるので、いい感じにジェネリクスを指定します。今回は<code>createNativeStackNavigator</code>で<br /> StackNavigatorを作成したので<code>NativeStackNavigationProp</code>を使用します。</p> <pre><code class="tsx">const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Home'>>(); </code></pre> <p>こうすることでエラーは消え、定義したルート名のみが指定できるようになりました。問題解決!</p> <h2 id="Bottom Tabs Navigator でヘッダーが二重に出る問題"><a href="#Bottom+Tabs+Navigator+%E3%81%A7%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%E3%81%8C%E4%BA%8C%E9%87%8D%E3%81%AB%E5%87%BA%E3%82%8B%E5%95%8F%E9%A1%8C">Bottom Tabs Navigator でヘッダーが二重に出る問題</a></h2> <p>まずは<a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/bottom-tab-navigator">Bottom Tabs Navigator</a>を<br /> 参考にBottom Tabsを用意します。</p> <pre><code class="tsx">type TabParamList = { Tab1: undefined; Tab2: undefined; Tab3: undefined; } const TabNavigator: React.FC = () => { return ( <Tab.Navigator initialRouteName="Tab1"> <Tab.Screen name="Tab1" component={Tab1Screen} /> <Tab.Screen name="Tab2" component={Tab2Screen} /> <Tab.Screen name="Tab3" component={Tab3Screen} /> </Tab.Navigator> ) } // Tab1Screenなどは省略・・・ </code></pre> <p>先程のホーム画面から<code>TabNavigator</code>へ遷移するよう設定し、いざ遷移!</p> <p><img src="https://github.com/kmgk/blog/blob/main/static/images/double-header.png?raw=true" alt="ヘッダーが二重になっている" /></p> <p>なぜかヘッダーが二重になっています。まずは下のヘッダー(タイトルがTab1の方)を消してみます。</p> <h3 id="下のヘッダー(Tab.Screen)を消す"><a href="#%E4%B8%8B%E3%81%AE%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%28Tab.Screen%29%E3%82%92%E6%B6%88%E3%81%99">下のヘッダー(Tab.Screen)を消す</a></h3> <p>これは<code>Tab.Navigator</code>のオプションで<code>headerShown</code>というパラメータがあるのでそれにfalseを渡してあげるだけです。</p> <pre><code class="tsx"><Tab.Navigator initialRouteName="Tab1" screenOptions=<span>{</span><span>{</span>headerShown: false<span>}</span><span>}</span>> </code></pre> <h3 id="上のヘッダー(TabNavigator)を消す"><a href="#%E4%B8%8A%E3%81%AE%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%28TabNavigator%29%E3%82%92%E6%B6%88%E3%81%99">上のヘッダー(TabNavigator)を消す</a></h3> <p>次に上のヘッダー(タイトルがTabの方)を消します。<br /> 先程のTab.Navigatorに渡したheaderShownは一度削除して、再びヘッダーが二重になるようにします。<br /> ここで、TabNavigationを呼び出しているNavigationContainerの方を見てみます。</p> <pre><code class="tsx"><NavigationContainer> <HomeStack.Navigator initialRouteName="Home"> ... <HomeStack.Screen name="Tab" component={TabNavigator} /> </HomeStack.Navigator> </NavigationContainer> </code></pre> <p><code>HomeStack.Screen</code>のヘッダーが表示されているので、それを消せば解決します。<br /> 先程と同様headerShownを渡します。Screenは<code>options</code>でオプションを受け取るので注意。(Navigatorは<code>screenOptions</code>)</p> <pre><code class="tsx"><HomeStack.Screen name="Tab" component={TabNavigator} options=<span>{</span><span>{</span> headerShown: false <span>}</span><span>}</span> /> </code></pre> <p>これで上のヘッダーを削除することができました。ちなみに両方に<code>headerShown: false</code>を渡すとヘッダーがなくなります。当然ですね。</p> <h2 id="参考文献"><a href="#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE">参考文献</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/typescript">Type checking with TypeScript - reactnavigation.org</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/bottom-tab-navigator#headershown">Bottom Tabs Navigator - reactnavigation.org</a></li> </ul> kmgk tag:crieit.net,2005:PublicArticle/16841 2021-04-13T16:45:08+09:00 2021-04-13T16:45:08+09:00 https://crieit.net/posts/38e8f748a2d53951f9604bea786870bf モバイルアプリ開発フレームワーク <p>現在、40億人を超えるモバイルユーザーがいるため、モバイルでオーディエンスとつながることができなければ、あなたは存在しません。人々はスマートフォンの使用をとても楽しんでいるので、技術のない日を過ごすために自分自身に挑戦する必要があります。スマートフォンは、電話、テキストメッセージ、メールのチェック、さらには娯楽まで、必要なものをすべて提供できます。そのため、モバイルアプリフレームワークが実現します。<br /> このような陰謀的なアプリケーションの舞台裏には、何千ものモバイルアプリ開発フレームワークがあります。ただし、モバイルアプリをうまく作るには、適切なモバイルアプリ開発フレームワーク、関連技術、プラットフォーム、データベースが不可欠です。とはいうものの、あまりにも多くの選択肢があり、どれかひとつに絞るのはこれまで以上に難しい状況です。そこでこの記事では、iOSとAndroid両方のプラットフォームで最も人気の高いモバイルアプリ開発フレームワークについて説明します。その前に、効率的な モバイルアプリ開発フレームワーク とはどのようなものかを簡単に見ていきましょう。</p> <p><strong>1. React Native</strong><br /> React Nativeは、AndroidアプリとiOSアプリの両方向けに構築された非常に人気のあるフレームワークです。特に、モバイルアプリの開発者は、より短いビルドサイクルでより高速なデプロイ時間で高性能アプリをビルドでき、予算にやさしいオプションです。さらに、React Nativeは、ビュー、テキスト、画像など、プラットフォームに依存しないネイティブコンポーネントのコアセットを提供し、すべてプラットフォームのネイティブUIビルディングブロックにマップされます。また、フルスタックに必須のJavaScriptもサポートしています。</p> <p><strong>2. Flutter</strong><br /> FlutterはGoogle が作ったオープンソースの<a target="_blank" rel="nofollow noopener" href="https://kaopiz.com/ja-news-cross-platform-framework-flutter-vs-react-native/">ネイティブアプリケーション開発フレームワーク</a>です。Dart という共通言語で開発を行います。詳しいデバッグができるので React Native で感じる「隔靴掻痒」感は少ないです。<br /> Flutterを使うことで同一のコードベースからAndroidやiOSアプリを構築でき、その見た目もネイティブアプリです。FlutterはGoogleによる導入が2015年でしたが、2018年12月の公式リリースまではベータ段階にとどまっていました。<br /> Flutterのユーザーは、携帯電話、テレビ、タブレット、ウェアラブルデバイス、スマートディスプレイ用のアプリを作成できます。<br /> ちなみに Flutter Studio は Flutter の UI 作成のために使えるサイトです。ビルドのためのものではありません。Flutter を採用するなら使うことになるのではないかと思われます。Flutter ではそれぞれの Native パートをプロジェクト含めることができる。</p> <p><strong>3. Ionic</strong><br /> Ionicは、クロスプラットフォームアプリケーションとともにインタラクティブハイブリッドおよびPWAを構築するのに役立ちます。このオープンソースフレームワークは、アプリケーションを作成するためのプレミアムサービスを提供します。さらに、Ionicは、Web、Android、およびiOS用のアプリケーションの構築をカバーしています。さらに、Ionicで作業している間は、常にアプリケーションを作成して、展開可能な場所に出荷できます。すぐに使用できる機能を備えているため、アプリケーション開発に最適です。</p> <p><strong>まとめ</strong><br /> 以上挙げた3つのフレームワーク以外、他にも<a target="_blank" rel="nofollow noopener" href="https://kaopiz.com/ja-news-mobile-application-development-frameworks-20202021/">モバイルアプリ開発フレームワーク</a>があります。どのフレームワークを選択するかは、要件、予算、技術的要件、そして短期的または長期的な成功の可能性に応じて、最も適したものを選ぶと良いでしょう。</p> hanhnh tag:crieit.net,2005:PublicArticle/16708 2021-03-01T21:16:04+09:00 2021-03-01T21:18:22+09:00 https://crieit.net/posts/React-Native-Realm React NativeでローカルデータベースRealmを使ったスマホアプリをつくる <p><a href="https://crieit.now.sh/upload_images/6edea335353bb5281df61e17ca2f1cab603cd2c9beea3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6edea335353bb5281df61e17ca2f1cab603cd2c9beea3.png?mw=700" alt="アイキャッチ" /></a></p> <blockquote> <p>本記事は<a target="_blank" rel="nofollow noopener" href="https://soji.dev/blog/realm-in-react-native">ブログ記事</a>の転載です。</p> </blockquote> <p>ユーザーが作成するデータをスマホにのみ保存し、サーバー上には保存しない React Native アプリを開発する場合、スマホ内のローカルデータベースにデータを保存することになります。本記事では、ローカルデータベースの1つである <a target="_blank" rel="nofollow noopener" href="https://realm.io/">Realm</a> を使った React Native アプリの開発方法を紹介します。</p> <h2 id="なぜサーバーではなくスマホに保存するのか"><a href="#%E3%81%AA%E3%81%9C%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F%E3%82%B9%E3%83%9E%E3%83%9B%E3%81%AB%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B%E3%81%AE%E3%81%8B">なぜサーバーではなくスマホに保存するのか</a></h2> <p>最近はFirebaseのようなmBaaS (Mobile Backend as a Service) が登場したことで、アプリ開発者がインフラを触るハードルが下がってきています。それでもなおサーバーではなくスマホにデータベースを保存するモチベーションとして、以下が挙げられます。</p> <ul> <li>サーバーの障害によってアプリが使えなくなることがない</li> <li>インターネットが使えない場所でもアプリを利用できる</li> <li>アプリ開発者がサーバーを維持管理する必要がなくなる <ul> <li>FirebaseのようなmBaaSでも、ユーザー増に伴い金銭的な負担が生じる</li> </ul></li> <li>大規模なデータ流出は起こらない <ul> <li>アプリ開発者のもとにユーザーのデータが集まらないため、データ流出の危険性は下がる</li> </ul></li> </ul> <h2 id="使用フレームワーク・ライブラリ"><a href="#%E4%BD%BF%E7%94%A8%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%BB%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA">使用フレームワーク・ライブラリ</a></h2> <p>本記事では以下のフレームワーク、ライブラリを利用します。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://reactnative.dev/">React Native</a> <ul> <li>クロスプラットフォーム対応のスマホアプリを開発できるフレームワーク。</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://realm.io/">Realm</a> <ul> <li>オフラインデータベース。開発元はMongoDB社により2019年買収。MongoDB社が提供する<a target="_blank" rel="nofollow noopener" href="https://www.mongodb.com/realm">MongoDB Realm</a>(mBaaS)を使うことで、複数端末間でデータベースを同期することもできるが、本記事の対象外。</li> </ul></li> </ul> <h2 id="SQLiteとの比較"><a href="#SQLite%E3%81%A8%E3%81%AE%E6%AF%94%E8%BC%83">SQLiteとの比較</a></h2> <p>スマホ内にデータベースを保存する方法として、RealmのほかにSQLiteがあります。RealmとSQLiteをざっくり比較すると、以下のようになります。</p> <ul> <li>RealmはSQLiteより軽い <ul> <li>Realmでは、クエリの結果は遅延読み込みされる</li> </ul></li> <li>RealmではSQLを書く必要がない</li> <li>ただし、RealmはExpoで使えない(SQLiteはExpoで使える)</li> </ul> <h2 id="本記事の前提"><a href="#%E6%9C%AC%E8%A8%98%E4%BA%8B%E3%81%AE%E5%89%8D%E6%8F%90">本記事の前提</a></h2> <p>まだReact Native 開発環境のセットアップをしていない場合、<a target="_blank" rel="nofollow noopener" href="https://reactnative.dev/docs/environment-setup">Setting up the development environment(React Nativeドキュメント)</a>にある "React Native CLI Quickstart" の内容を読み、指示に従ってください。</p> <p>また、本記事の内容は、以下の環境で動作確認しています。</p> <ul> <li>macOS Big Sur 11.2.1</li> <li>React Native 0.63.4</li> <li>Node.js v14.16.0</li> <li>Xcode 12.4</li> <li>Simulator 上の iPhone 11 / iOS 14.4</li> </ul> <h2 id="Realmの準備"><a href="#Realm%E3%81%AE%E6%BA%96%E5%82%99">Realmの準備</a></h2> <h3 id="React Native アプリの作成"><a href="#React+Native+%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E4%BD%9C%E6%88%90">React Native アプリの作成</a></h3> <p>ここでは、MyRealmAppという名前のReact Nativeアプリを作成してみます。なお、ExpoではRealmを使うことができませんので注意してください。</p> <pre><code class="bash">$ npx react-native init MyRealmApp </code></pre> <h3 id="Realmのインストール"><a href="#Realm%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Realmのインストール</a></h3> <pre><code class="bash">$ cd MyRealmApp $ npm install realm $ cd ios && pod install && cd .. </code></pre> <p>PCにインストールされているNode.jsのバージョンが古いとエラーになることがあります。</p> <p>もし、上記の<code>npm install realm</code>コマンドを実行した結果、<code>The N-API version of this Node instance is (数値). This module supports N-API version(s) (数値). This Node instance cannot run this module.</code> というエラーが表示された場合、Node.js のバージョンを上げてください。</p> <h2 id="スキーマの定義とRealmの初期化"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%81%AE%E5%AE%9A%E7%BE%A9%E3%81%A8Realm%E3%81%AE%E5%88%9D%E6%9C%9F%E5%8C%96">スキーマの定義とRealmの初期化</a></h2> <p>本記事では例としてタスク管理アプリを設計してみます。タスクを表現する<code>Task</code> クラスと、各 Task にはサブタスクを設定できるよう、<code>SubTask</code> クラスを定義します。</p> <p>まず、作成した React Native プロジェクト内に src フォルダを作成します。src フォルダは、 <code>App.js</code> と同階層のフォルダに作成してください。次に、src フォルダ内に以下のファイルを作成します。ファイル名は <code>realm.js</code> とします。</p> <h4 id="./src/realm.js"><a href="#.%2Fsrc%2Frealm.js">./src/realm.js</a></h4> <pre><code class="js">// Taskの定義 const taskSchema = { name: 'Task', primaryKey: '_id', properties: { _id: 'objectId', // 'string' や 'int' でも OK name: 'string', description: 'string?', // ?をつけると optional isDone: 'bool', createdAt: 'date', subTasks: 'SubTask[]', // クラス名 + '[]' で1対多のリレーションを設定できる }, }; // SubTaskの定義 const subTaskSchema = { name: 'SubTask', primaryKey: '_id', properties: { _id: 'objectId', name: 'string', isDone: 'bool', createdAt: 'date', }, }; // Realmの初期化 export const openRealm = () => { const config = { schema: [taskSchema, subTaskSchema], schemaVersion: 1, // スキーマを変更したらインクリメントする(後述) }; return new Realm(config); }; export {BSON} from 'realm'; </code></pre> <h2 id="データベースの操作"><a href="#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E6%93%8D%E4%BD%9C">データベースの操作</a></h2> <p>React NativeでRealmを使う前に、まずRealm単体でデータベースを操作する方法を見ていきます。</p> <p>このセクションのコードは、作成した React Native プロジェクトのフォルダに入っている <code>App.js</code> 内の適当な場所に書いた上で <code>npx react-native run-ios</code> コマンドを実行すると Simulator 上で動作確認できます。</p> <p>(<code>npx react-native run-ios</code> コマンドがエラーで失敗する場合、<a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/66019068/event2-event-config-h-file-not-found">Flipperのバージョンを変更してみてください(Stack Overflow 英語版)</a>)</p> <h3 id="オブジェクトの新規作成"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E6%96%B0%E8%A6%8F%E4%BD%9C%E6%88%90">オブジェクトの新規作成</a></h3> <pre><code class="js">import {openRealm, BSON} from './src/realm'; const realm = openRealm(); // 更新系はすべて realm.write(() => { }) (=トランザクション)内に書く realm.write(() => { // サブタスクを作成しない場合 realm.create('Task', { _id: new BSON.ObjectId(), name: 'タスクの名前', isDone: false, createdAt: new Date(), }); // サブタスクを作成する場合 realm.create('Task', { _id: new BSON.ObjectId(), name: 'タスクの名前', isDone: false, createdAt: new Date(), subTasks: [ { _id: new BSON.ObjectId(), name: 'サブタスクの名前', isDone: false, createdAt: new Date(), }, ], }); }); </code></pre> <h3 id="オブジェクトの取得"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%8F%96%E5%BE%97">オブジェクトの取得</a></h3> <p><code>filtered</code> で利用可能なクエリについては<a target="_blank" rel="nofollow noopener" href="https://docs.mongodb.com/realm/react-native/query-engine/">公式ドキュメント</a>を参照してください。</p> <pre><code class="js">import {openRealm} from './src/realm'; const realm = openRealm(); // タスクを全部取得 const tasks = realm.objects('Task'); console.log(tasks[0].name); // 「タスクの名前」と表示される console.log(tasks[1].subTasks[0].name); // 「サブタスクの名前」と表示される // フィルタの例 — 完了しているタスクのみ取得 const done = tasks.filtered('isDone == true'); console.log(`完了しているタスクは${done.length}件です。`); // ソートの例 — 名前順で取得 const sorted = tasks.sorted('name'); console.log(sorted[0].name); </code></pre> <h3 id="オブジェクトの更新"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E6%9B%B4%E6%96%B0">オブジェクトの更新</a></h3> <p>JavaScriptのオブジェクトを操作するのと同じ方法でRealm上のデータを更新できます。</p> <pre><code class="js">import {openRealm} from './src/realm'; const realm = openRealm(); realm.write(() => { // 更新対象のタスク const task = realm.objects('Task')[0]; // 更新 task.name = '新しいタスクの名前'; task.isDone = !task.isDone; }); </code></pre> <h3 id="オブジェクトの削除"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%89%8A%E9%99%A4">オブジェクトの削除</a></h3> <pre><code class="js">import {openRealm} from './src/realm'; const realm = openRealm(); realm.write(() => { // 削除対象のタスク const task = realm.objects('Task')[0]; if (task) { // まずサブタスクを削除 realm.delete(task.subTasks); // 削除 realm.delete(task); } }); </code></pre> <h2 id="React NativeでRealmを使う"><a href="#React+Native%E3%81%A7Realm%E3%82%92%E4%BD%BF%E3%81%86">React NativeでRealmを使う</a></h2> <p>React Nativeアプリ内でTaskを操作できるようにするためのコンテキストと React Hook を作ります。</p> <p>以下の例は、GitHubの <a target="_blank" rel="nofollow noopener" href="https://github.com/mongodb-university/realm-tutorial-react-native">mongodb-university/realm-tutorial-react-native</a> リポジトリ内のコードを参考に、本記事用に書き換えたものです。</p> <p>まず<code>src</code> フォルダ内に<code>providers</code> フォルダを作成します。<code>providers</code>フォルダ内に<code>TaskProvider.js</code>という名前でファイルを作成し、以下のコードを書きます。このコードの詳細については、コード中のコメントを参照してください。</p> <h4 id="./src/providers/TasksProvider.js"><a href="#.%2Fsrc%2Fproviders%2FTasksProvider.js">./src/providers/TasksProvider.js</a></h4> <pre><code class="jsx">import React, {useContext, useState, useEffect, useRef} from 'react'; import {openRealm, BSON} from '../realm'; const TasksContext = React.createContext(null); const TasksProvider = ({children}) => { const [tasks, setTasks] = useState([]); const realmRef = useRef(null); useEffect(() => { realmRef.current = openRealm(); const tasks = realmRef.current.objects('Task').sorted('createdAt', true); setTasks(tasks); // Task のデータが更新されたら setTasks する tasks.addListener(() => { const tasks = realmRef.current.objects('Task').sorted('createdAt', true); setTasks(tasks); }); return () => { // クリーンアップ if (realmRef.current) { realmRef.current.close(); } }; }, []); // タスクの新規作成 const createTask = (newTaskName) => { const projectRealm = realmRef.current; projectRealm.write(() => { projectRealm.create('Task', { _id: new BSON.ObjectId(), name: newTaskName || '新しいタスク', isDone: false, createdAt: new Date(), }); }); }; // タスクの isDone を更新する const setIsTaskDone = (task, isDone) => { const projectRealm = realmRef.current; projectRealm.write(() => { task.isDone = isDone; }); }; // タスクを削除する const deleteTask = (task) => { const projectRealm = realmRef.current; projectRealm.write(() => { projectRealm.delete(task); }); }; // useTasks フックで Task を操作できるようにする return ( <TasksContext.Provider value=<span>{</span><span>{</span> createTask, deleteTask, setIsTaskDone, tasks, <span>}</span><span>}</span>> {children} </TasksContext.Provider> ); }; // Task を操作するための React Hook const useTasks = () => { const task = useContext(TasksContext); if (task == null) { throw new Error('useTasks() called outside of a TasksProvider?'); } return task; }; export {TasksProvider, useTasks}; </code></pre> <p>以上で作成したTasksProviderをApp.jsに追加し、useTasksフックをコンポーネント内で使ってみます。ここでは、<code>src</code> フォルダ内に<code>components</code> フォルダを作成し、その中に<code>Main.js</code> というファイルでコンポーネントを定義します。</p> <h4 id="./App.js"><a href="#.%2FApp.js">./App.js</a></h4> <pre><code class="jsx">import React from 'react'; import {TasksProvider} from './src/providers/TasksProvider'; import {Main} from './src/components/Main'; import {StatusBar} from 'react-native'; const App = () => { return ( <TasksProvider> <StatusBar barStyle="light-content" /> <Main /> </TasksProvider> ); }; export default App; </code></pre> <h4 id="./src/components/Main.js"><a href="#.%2Fsrc%2Fcomponents%2FMain.js">./src/components/Main.js</a></h4> <pre><code class="jsx">import React, {useState, useCallback} from 'react'; import { SafeAreaView, FlatList, View, Text, TouchableOpacity, TextInput, KeyboardAvoidingView, StyleSheet, TouchableWithoutFeedback, Keyboard, } from 'react-native'; import {useTasks} from '../providers/TasksProvider'; // コンポーネント間の余白を作るための関数 const spacer = (size) => { return <View style=<span>{</span><span>{</span>height: size, width: size<span>}</span><span>}</span> />; }; export const Main = () => { const [inputText, setInputText] = useState(''); const {createTask, deleteTask, setIsTaskDone, tasks} = useTasks(); const onSubmitEditing = useCallback( (event) => { setInputText(event.nativeEvent.text); createTask(inputText); setInputText(''); }, [inputText, setInputText, createTask], ); return ( <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.container}> <View style={styles.header}> <SafeAreaView /> <View style={styles.headerContent}> {spacer(15)} <Text accessibilityRole="header" style={styles.headerTitle}> My Tasks </Text> {spacer(15)} <TextInput placeholder="Add Task..." onSubmitEditing={onSubmitEditing} value={inputText} onChange={(e) => setInputText(e.nativeEvent.text)} style={styles.headerInput} /> {spacer(24)} </View> </View> <TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}> {tasks.length > 0 ? ( <FlatList style={styles.tasksContainer} contentContainerStyle={styles.tasksContentContainer} data={tasks} keyExtractor={(item) => item._id.toHexString()} renderItem={({item}) => ( <View style={styles.taskItem}> <TouchableOpacity style={styles.chechboxContainer} onPress={() => setIsTaskDone(item, !item.isDone)}> <View style={[ styles.checkbox, { borderColor: item.isDone ? '#2563EB' : '#60A5FA', backgroundColor: item.isDone ? '#2563EB' : '#fff', }, ]}> {item.isDone && <Text style={styles.checkboxIcon}>✓</Text>} </View> </TouchableOpacity> <View style={styles.taskContent}> <Text style={[ styles.taskName, { textDecorationLine: item.isDone ? 'line-through' : 'none', color: item.isDone ? '#9CA3AF' : '#111827', }, ]}> {item.name} </Text> </View> <TouchableOpacity style={styles.deleteButton} onPress={() => deleteTask(item)}> <View> <Text style={styles.deleteButtonText}>Delete</Text> </View> </TouchableOpacity> </View> )} /> ) : ( <View style={styles.emptyContent}> <Text style={styles.emptyMessage}>No Tasks</Text> </View> )} </TouchableWithoutFeedback> </KeyboardAvoidingView> ); }; // スタイル const styles = StyleSheet.create({ container: {flex: 1}, header: { backgroundColor: '#2563EB', }, headerContent: {paddingHorizontal: 24}, headerTitle: {color: 'white', fontSize: 32, fontWeight: 'bold'}, headerInput: { borderRadius: 6, height: 40, paddingHorizontal: 12, marginHorizontal: -12, fontSize: 18, backgroundColor: '#fff', shadowColor: '#000', shadowOffset: { width: 0, height: 2.5, }, shadowOpacity: 0.3, shadowRadius: 2.4, elevation: 4, }, tasksContainer: {flex: 1}, tasksContentContainer: {paddingBottom: 20}, taskItem: { borderBottomWidth: 1, borderBottomColor: '#E5E7EB', flexDirection: 'row', alignItems: 'center', }, chechboxContainer: {padding: 20}, checkbox: { width: 25, height: 25, borderRadius: 12, borderWidth: 1.5, alignItems: 'center', justifyContent: 'center', }, checkboxIcon: {color: 'white', fontWeight: 'bold', fontSize: 18}, taskContent: {flex: 1}, taskName: { fontSize: 20, fontWeight: '600', }, deleteButton: {padding: 20}, deleteButtonText: {color: '#EF4444'}, emptyContent: { justifyContent: 'center', alignItems: 'center', flex: 1, }, emptyMessage: { fontSize: 20, fontWeight: '500', color: '#9CA3AF', }, }); </code></pre> <h2 id="動作確認"><a href="#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">動作確認</a></h2> <p>ここまでできたら、一度動作確認してみます。今回は Simulator 上の iPhone で動作確認します。以下のコマンドで、シミュレータを立ち上げます。</p> <p>(以下のコマンドがエラーで失敗する場合、<a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/66019068/event2-event-config-h-file-not-found">Flipperのバージョンを変更してみてください(Stack Overflow 英語版)</a>)</p> <pre><code class="bash">$ npx react-native run-ios </code></pre> <p>以下の動画のように動作すれば成功です。もしエラーが発生する場合、「DerivedDataの削除」「iosフォルダ内で<code>pod install</code> 再実行」などをお試しください(詳細はエラーメッセージでググってみてください)。</p> <p>ここまで書いてきたコードでは、タスクの編集ができません。タスク名を自由に編集できるようにしてみると良いかもしれません(本記事では省略します)。</p> <h2 id="スキーマ変更時の注意点(schemaVersionとマイグレーション)"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E5%A4%89%E6%9B%B4%E6%99%82%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9%28schemaVersion%E3%81%A8%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%29">スキーマ変更時の注意点(schemaVersionとマイグレーション)</a></h2> <p>アプリストアで配信中のアプリをアップデートする際、データベースのスキーマが変更になる場合、<code>schemaVersion</code> をインクリメントする必要があります。また、必要に応じて、旧スキーマのデータを新スキーマのデータへ移行する処理(マイグレーション)を書く必要があります。</p> <p>例として、Taskの<code>isDone</code> を変更してみます。ここまで、タスクの完了状況は<code>isDone</code>で管理してきました。<code>isDone</code> はタスクの完了・未完了しか表すことができないため、新しく<code>status</code> として、タスクの状態を以下の3つに分類できるようにしてみます。</p> <ul> <li>Open : タスク未着手</li> <li>InProgress : タスク進行中</li> <li>Complete : 完了</li> </ul> <h3 id="スキーマを書き換える"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%82%92%E6%9B%B8%E3%81%8D%E6%8F%9B%E3%81%88%E3%82%8B">スキーマを書き換える</a></h3> <p><code>./realm.js</code> に書いた Task のスキーマ定義を以下のように変更します。この作業はスキーマ定義を直接書き換えます。</p> <pre><code class="js">// 略 const taskSchema = { name: "Task", primaryKey: "_id", properties: { _id: "objectId", name: "string", description: "string?", // isDone: 'bool', status: "string", // isDone を削除し status を追加 createdAt: "date", subTasks: "SubTask[]", }, }; // 略 </code></pre> <h3 id="マイグレーション処理を書く"><a href="#%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%87%A6%E7%90%86%E3%82%92%E6%9B%B8%E3%81%8F">マイグレーション処理を書く</a></h3> <p><code>./realm.js</code> ファイルを保存し、React Native開発環境を再度実行すると、コンソールに以下のようなエラーが表示されます。</p> <pre><code>Error: Migration is required due to the following errors: - Property 'Task.isDone' has been removed. - Property 'Task.status' has been added. </code></pre> <p>このエラーメッセージを日本語に訳すと以下のようになります。</p> <blockquote> <p>エラー: 以下のエラーのため、マイグレーションが必要です<br /> - プロパティ 'Task.isDone' が削除されました<br /> - プロパティ 'Task.status' が追加されました</p> </blockquote> <p>このようなエラーが表示された場合、<code>./realm.js</code> ファイルにある <code>openRealm</code> 関数内の <code>config</code> 定数を以下のように書き換えます。</p> <pre><code class="js">// 略 // Realmの初期化 export const openRealm = () => { const config = { schema: [taskSchema, subTaskSchema], // schemaVersion: 1, schemaVersion: 2, // ① schemaVersion を 1 → 2 へ変更 // ②マイグレーション処理を追加 migration: (oldRealm, newRealm) => { // 現在保存されているデータの schemaVersion が 2 未満の場合に実行 if (oldRealm.schemaVersion < 2) { const oldObjects = oldRealm.objects('Task'); const newObjects = newRealm.objects('Task'); // 全TaskデータのisDoneをstatusに変換 for (const objectIndex in oldObjects) { const oldObject = oldObjects[objectIndex]; const newObject = newObjects[objectIndex]; newObject.status = oldObject.isDone ? 'Complete' : 'Open'; } } }, }; return new Realm(config); }; // 略 </code></pre> <p>このファイルを保存し、再度アプリを実行すると、マイグレーションが実行され、データベースが更新されます。まだ isDone がコード上で使われているため、必要に応じて status を使うように修正を行ってください(本記事では省略します)。</p> <p>ここでのポイントは以下の2点です。</p> <h4 id="① schemaVersion を 1 → 2 へ変更"><a href="#%E2%91%A0+schemaVersion+%E3%82%92+1+%E2%86%92+2+%E3%81%B8%E5%A4%89%E6%9B%B4">① schemaVersion を 1 → 2 へ変更</a></h4> <p>Realm にデータベーススキーマが変更になったことを伝えるため、schemaVersion をインクリメントします。schemaVersion は、データベーススキーマに変更がある場合のみ変更します。一度 schemaVersion の値を増やした場合、この数値を減らさないでください。</p> <h4 id="② マイグレーション処理を追加"><a href="#%E2%91%A1+%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%87%A6%E7%90%86%E3%82%92%E8%BF%BD%E5%8A%A0">② マイグレーション処理を追加</a></h4> <p>ユーザーのスマホ内に保存されている古いスキーマのデータベース内のデータを、新しいスキーマのデータベースに変換する処理を書きます。多くの場合、oldRealmのデータをもとに、対応するnewRealm内のデータを全部書き換えるようなコードになると思います。もしくは、optional ではない新しいプロパティが追加された場合、そのプロパティの初期値を設定するコードになると思います。</p> <h2 id="RealmデータベースGUIで操作できる「Realm Studio」"><a href="#Realm%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9GUI%E3%81%A7%E6%93%8D%E4%BD%9C%E3%81%A7%E3%81%8D%E3%82%8B%E3%80%8CRealm+Studio%E3%80%8D">RealmデータベースGUIで操作できる「Realm Studio」</a></h2> <p>最後に、Realm Studioを紹介します。Realm Studio はWindows、Linux、macOSに対応したRealmデータベースの管理アプリです。Realm Studio を使うことで、Realmデータベースの中身をGUI上で確認・操作することができます。</p> <p>Realm Studioは、Realm 公式サイト内にある<a target="_blank" rel="nofollow noopener" href="https://docs.realm.io/sync/realm-studio">Realm Studioのページ</a>からダウンロードできます。Realm Studio を初めて起動するとメールアドレスの登録画面が表示されるのでメールアドレスを登録します。その後、以下の画面が表示されます。</p> <p><a href="https://crieit.now.sh/upload_images/8ede1c18125488061693d34088d212bf603cd9c3b4a55.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8ede1c18125488061693d34088d212bf603cd9c3b4a55.png?mw=700" alt="Realm Studioのスタート画面" /></a></p> <p>この画面が表示されたら [Open Realm file] をクリックし、Realm ファイルを開きます。Realm ファイルのパスは、以下のコードを<code>App.js</code>等に書いて実行することで確認できます(コンソールにファイルパスが表示されます)。</p> <pre><code class="js">import {openRealm} from './src/realm.js'; console.log(openRealm().path); </code></pre> <p>無事ファイルを開けると、以下のような画面になります。この画面から、データの確認・編集ができます。</p> <p><a href="https://crieit.now.sh/upload_images/93d1f6c129913c5f4eaba69e564ac993603cd9d1b9036.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/93d1f6c129913c5f4eaba69e564ac993603cd9d1b9036.png?mw=700" alt="Realm Studioの画面" /></a></p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>React Nativeアプリでオフラインデータを扱うライブラリとしてRealmを紹介してみました。クラウド上にデータを置かないアプリ開発の参考になれば幸いです。</p> <h2 id="参考文献"><a href="#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE">参考文献</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.mongodb.com/realm/react-native/">MongoDB Realm React Native SDK</a></li> </ul> SofPyon tag:crieit.net,2005:PublicArticle/15879 2020-04-29T12:27:54+09:00 2020-04-29T20:21:02+09:00 https://crieit.net/posts/ReactNative-react-intl ReactNativeアプリを多言語化する方法 (react-intl) <p><a href="https://crieit.now.sh/upload_images/fe830e8631a6c3778511aa813c7fdf665ea8f37986446.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/fe830e8631a6c3778511aa813c7fdf665ea8f37986446.png?mw=700" alt="image" /></a></p> <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>アプリの国際化に伴い、多言語に対応する必要が出てくると思います。<br /> Reactを使っている場合、react-intlを利用すると簡単に実現することが可能です。</p> <p>この記事では以下のことをやります。</p> <ul> <li>React Nativeで作成したアプリを多言語に対応させる</li> <li>端末の言語設定に沿って翻訳する</li> <li>英語と日本語に対応する</li> <li>英語の翻訳情報がない場合は日本語が表示されるようにする</li> <li>サポート外言語が設定されている場合は日本語が表示されるようにする</li> </ul> <h1 id="react-intlとは?"><a href="#react-intl%E3%81%A8%E3%81%AF%EF%BC%9F">react-intlとは?</a></h1> <p>Reactアプリを国際化するためのライブラリです。<br /> 文字列、日付、数字など様々なフォーマットに対応しています。<br /> フックが用意されていたり、結構柔軟に対応できます。</p> <h1 id="環境情報"><a href="#%E7%92%B0%E5%A2%83%E6%83%85%E5%A0%B1">環境情報</a></h1> <ul> <li>macOS</li> <li>React Native (0.60.5) </li> <li>Typescript (^3.8.3)</li> <li>react-intl (^4.1.1)</li> <li>yarn</li> <li>cocoapods</li> </ul> <h1 id="セットアップ"><a href="#%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97">セットアップ</a></h1> <p>サンプルプロジェクト作成</p> <pre><code>$ npx react-native init LocalizationSample --version 0.60.5 --template react-native-template-typescript $ yarn add react-intl $ yarn add intl $ yarn add lodash @types/lodash $ yarn add react-native-localize $ cd ios $ pod install </code></pre> <ul> <li>intlは、ReactNativeでreact-intlを利用する時に必要なので入れます。</li> <li>react-native-localizeは、端末から言語設定を取得するために入れます。</li> <li>lodashは、あとでincludesメソッドを使いたいので入れます。</li> </ul> <p>動作確認</p> <pre><code>$ cd LocalizationSample $ npx react-native run-ios $ npx react-native run-android </code></pre> <p>iosとandroidのシュミレーター で「Welcome to React」と表示されたらOKです。<br /> ※ androidは先にシュミレータを起動しておく必要があります</p> <h1 id="ロケールファイルを作成する"><a href="#%E3%83%AD%E3%82%B1%E3%83%BC%E3%83%AB%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">ロケールファイルを作成する</a></h1> <p>プロジェクト直下にja.jsonとen.jsonを作成してください。</p> <p>ja.json</p> <pre><code>{ "title": "ホーム", "name": "{name} 様", "message": "こんにちは" } </code></pre> <p>en.json</p> <pre><code>{ "title": "HOME", "name": "Mr/Ms {name}" } </code></pre> <ul> <li>{}で文字列を囲むと、その文字列をkeyに値を外から差し込めるようになります。</li> <li>en.json側にmessageが存在しませんが、あとで日本語にフォールバックされることを確認したいのでこうしています。</li> </ul> <h1 id="言語設定を取得する関数を用意する"><a href="#%E8%A8%80%E8%AA%9E%E8%A8%AD%E5%AE%9A%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0%E3%82%92%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B">言語設定を取得する関数を用意する</a></h1> <p>プロジェクト直下にi18n.tsxを作成して、言語設定を取得するgetLocale()を記述します。</p> <pre><code class="typescript">import * as React from 'react'; import * as RNLocalize from 'react-native-localize'; import {includes} from 'lodash'; const SUPPORTED_LOCALE = ['ja', 'en']; const DEFAULT_LOCALE = 'ja'; const getLocale = (): string => { const locales = RNLocalize.getLocales(); const languageCode = locales[0].languageCode; if (includes(SUPPORTED_LOCALE, languageCode)) { return languageCode; } return DEFAULT_LOCALE; }; </code></pre> <ul> <li>SUPPORTED_LOCALEの値がアプリで対応する言語です。</li> <li>RNLocalize.getLocales() から端末の言語設定を取得しています。</li> <li>取得した言語がサポート外ならばデフォルトロケールを返すようにしています。</li> </ul> <h1 id="翻訳情報を返す関数を用意する"><a href="#%E7%BF%BB%E8%A8%B3%E6%83%85%E5%A0%B1%E3%82%92%E8%BF%94%E3%81%99%E9%96%A2%E6%95%B0%E3%82%92%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B">翻訳情報を返す関数を用意する</a></h1> <p>まず、先ほど作成したi18n.tsxでロケールファイルをインポートします。</p> <pre><code class="typescript">import ja from './ja.json'; import en from './en.json'; ... </code></pre> <p>次に、翻訳情報を返すgetMesseges()を追記します。</p> <pre><code class="typescript">... const getMessages = (locale: string): {[key: string]: string} => { switch (locale) { case 'ja': return ja; case 'en': return { ...getMessages('ja'), ...en, }; default: throw new Error('unknown locale'); } }; ... </code></pre> <ul> <li>'ja'が引数に指定された場合、日本語の翻訳情報を返します。</li> <li>'en'が引数に指定された場合、英語の翻訳情報を返します。ただ、欠落している部分は日本語が入るようになっています。こうすることで英語がないとき日本語にフォールバックすることができます。</li> </ul> <h1 id="IntlProviderを設定する"><a href="#IntlProvider%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B">IntlProviderを設定する</a></h1> <p>react-intlを利用するため、アプリのルートコンポーネントを<code><IntlProvider></code>でラップする必要があります。<br /> また、Intlポリフィルも合わせてimportする必要があるので、i18n.tsxで<code><IntlProvider></code>をラップした<code><IntlProviderWrapper></code>作成して、それをApp.tsxで呼び出すようにしたいと思います。</p> <p>まず、i18n.tsxの一行目でポリフィルをインポートします。</p> <pre><code class="typescript">import 'intl'; import 'intl/locale-data/jsonp/ja'; import 'intl/locale-data/jsonp/en'; ... </code></pre> <ul> <li><code>import 'intl/locale-data/jsonp/ja';</code>は、サポートする言語が増えるたび、同様に追加する必要があります。今回は日本語(ja)と英語(en)だけです。これをインポートしないと起動したときにIntlが見つからないよと怒られます。</li> </ul> <p>次に、i18n.tsxに<code><IntlProvider></code>をラップした<code><IntlProviderWrapper></code>を返すコンポーネントを作成します。<br /> childrenを受け取るようにするので、指定する型(ReactNode)をインポートしておきます。</p> <pre><code class="typescript">... import {ReactNode} from 'react'; ... export const IntlProviderWrapper = ({children}: {children: ReactNode}) => { const locale = getLocale(); return ( <IntlProvider locale={locale} messages={getMessages(locale)}> {children} </IntlProvider> ); }; ... </code></pre> <ul> <li>localeには翻訳するロケールを指定します</li> <li>messagesにはロケールファイルから取得したJSONオブジェクトを渡します。こうすることで、<code><IntlProvider></code>でラップしたコンポーネントで、keyに紐づく値を取得することができるようになります。</li> </ul> <p>次に、App.tsxを開いて、作成したでルートコンポーネントをラップします。</p> <pre><code class="typescript">import * as React from 'react'; import {SafeAreaView, Text} from 'react-native'; import {IntlProviderWrapper} from './i18n'; const App = () => { return ( <SafeAreaView> <Text>{'Hello'}</Text> </SafeAreaView> ); }; export default () => { return ( <IntlProviderWrapper> <App /> </IntlProviderWrapper> ); }; </code></pre> <p>これで設定DONE。</p> <h1 id="実際に翻訳してみる"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E7%BF%BB%E8%A8%B3%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">実際に翻訳してみる</a></h1> <p><code><FormattedMessage></code>を使うことで文字列の翻訳ができます。<br /> <code>FormattedMessage</code>をインポートして、<code><SafeAreaView></code>の中身を以下のように変更します。</p> <pre><code class="typescript">... import {FormattedMessage} from 'react-intl'; ... const App = () => { return ( <SafeAreaView> <Text> <FormattedMessage id={'title'} /> </Text> <Text> <FormattedMessage id={'name'} values=<span>{</span><span>{</span>name: '太郎'<span>}</span><span>}</span> /> </Text> <Text> <FormattedMessage id={'message'} /> </Text> </SafeAreaView> ); }; ... </code></pre> <ul> <li>idには、keyを指定します。</li> <li>valuesには、差し込む値を指定します。ちなみに、HTMLも差し込めます。<br /> ※ こちらには、文字列以外の対応方法も載っています。</li> </ul> <h1 id="実行結果"><a href="#%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C">実行結果</a></h1> <p>端末の言語設定を英語にして実行した場合のキャプチャです。<br /> en.jsonには、<code>message</code>というkeyがないので日本語にフォールバックされてます。</p> <p><a href="https://crieit.now.sh/upload_images/ab46b254c9c66739dd121dcf1554eeff5ea96319e8fc5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ab46b254c9c66739dd121dcf1554eeff5ea96319e8fc5.png?mw=700" alt="image" /></a></p> <h1 id="メソッドの引数などに翻訳した文字列を使いたい場合"><a href="#%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%BC%95%E6%95%B0%E3%81%AA%E3%81%A9%E3%81%AB%E7%BF%BB%E8%A8%B3%E3%81%97%E3%81%9F%E6%96%87%E5%AD%97%E5%88%97%E3%82%92%E4%BD%BF%E3%81%84%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88">メソッドの引数などに翻訳した文字列を使いたい場合</a></h1> <p><code>useIntl()</code>フックを使うことで実現できます。</p> <pre><code>import {useIntl} from 'react-intl'; ... const {formatMessage} = useIntl(); const name = formatMessage({id: 'name'}, {name: '太郎'}) </code></pre> <p>みたいに使えます。</p> <h1 id="最後に"><a href="#%E6%9C%80%E5%BE%8C%E3%81%AB">最後に</a></h1> <p>最後まで読んでいただきありがとうございます。<br /> 誰かの参考になれば嬉しいです。</p> <p>この記事に書いたコードは、<a target="_blank" rel="nofollow noopener" href="https://github.com/kashimura0001/localization-sample">GitHub</a>に全て上げました。</p> <h1 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/formatjs/react-intl">https://github.com/formatjs/react-intl</a></p> かしむら tag:crieit.net,2005:PublicArticle/14995 2019-05-18T21:52:15+09:00 2019-05-18T21:52:15+09:00 https://crieit.net/posts/ReactNative ReactNativeアプリを作り始めました。 <h1 id="背景"><a href="#%E8%83%8C%E6%99%AF">背景</a></h1> <p>アニメ視聴遅れ管理サービスを作っているのですが、<a href="https://crieit.net/boards/annict-access/RN">ReactNativeアプリ</a>で好きなアニメランキングを作りはじめました。<br /> これがランキングを作る際の番組選択画面にあたります。<br /> (warningが出てるのは許してください....。)</p> <p><a href="https://crieit.now.sh/upload_images/73aa3ebbbfecf178ae23f56d1cbdbed45cdffa0224711.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/73aa3ebbbfecf178ae23f56d1cbdbed45cdffa0224711.jpg?mw=700" alt="image" /></a></p> <h1 id="GraphQLでサーバからデータを取得する"><a href="#GraphQL%E3%81%A7%E3%82%B5%E3%83%BC%E3%83%90%E3%81%8B%E3%82%89%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">GraphQLでサーバからデータを取得する</a></h1> <p>以前、<a href="https://crieit.net/posts/React-GraphQL-API">ReactでGraphQL APIにアクセスする</a>という記事を書いたので今回は割愛します。</p> <p>今回作成したクエリはこちら。<br /> <strong>${year}</strong> で取得したい年を渡しています。<br /> クエリの内容としては、${year}で指定した番組のタイトル、画像URL、annict上の番組ID、シーズンを視聴者数順にソートして返します。</p> <pre><code class="javascript">let query = gql`{ searchWorks( seasons:["${year}-spring","${year}-summer","${year}-autumn","${year}-winter"], orderBy: { field: WATCHERS_COUNT, direction: DESC } ){ edges{ node{ title image{recommendedImageUrl} annictId seasonName seasonYear } } } }` </code></pre> <h1 id="長すぎるタイトルを省略する"><a href="#%E9%95%B7%E3%81%99%E3%81%8E%E3%82%8B%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB%E3%82%92%E7%9C%81%E7%95%A5%E3%81%99%E3%82%8B">長すぎるタイトルを省略する</a></h1> <p>TextコンポーネントにこんなPropsがあるらしい。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/kondoakio/items/5a27aaf8e6a57b1fc106">【React Native】三点リーダーでテキストを省略</a></p> <p>今回使ったProps。</p> <pre><code class="javascript">numberOfLines={3} ellipsizeMode="tail" </code></pre> <h1 id="番組データを3カラムで表示する"><a href="#%E7%95%AA%E7%B5%84%E3%83%87%E3%83%BC%E3%82%BF%E3%82%923%E3%82%AB%E3%83%A9%E3%83%A0%E3%81%A7%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B">番組データを3カラムで表示する</a></h1> <p>deprecatedになっているListViewを使っているところは気になりますが、FlatListに読み替えてしまえばかなりの良記事だと思います。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/mat_aki/items/2db69acf61cf15ad70de">React Native の ListView で2カラムの表示を簡単に</a></p> <h1 id="番組画像に作品タイトルをオーバーレイさせる"><a href="#%E7%95%AA%E7%B5%84%E7%94%BB%E5%83%8F%E3%81%AB%E4%BD%9C%E5%93%81%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB%E3%82%92%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%AC%E3%82%A4%E3%81%95%E3%81%9B%E3%82%8B">番組画像に作品タイトルをオーバーレイさせる</a></h1> <p>元々UIに疎かった私。<br /> 画像に半透明のレイヤーを被せてその上に文字を表示するのを何というかということも知らなかったので、「画像 文字 のせる」というところからググりはじめて「オーバーレイ」と呼ぶことを知ったので、「react native overlay text」でググると...。<br /> <a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/49250047/how-to-place-a-text-over-image-in-react-native">How to place a text over image in react native?<br /> </a></p> <p>Imageコンポーネントはchildrenを持てないようなので、<br /> <strong>ImageBackGround</strong>コンポーネントを使います。</p> <h1 id="実装まとめ"><a href="#%E5%AE%9F%E8%A3%85%E3%81%BE%E3%81%A8%E3%82%81">実装まとめ</a></h1> <p>今回行った実装の一部です。<br /> Imageコンポーネントに画像URLを渡していますが、<br /> 空文字やnullなどは怒られてしまうので条件分岐をさせて回避します。<br /> これで作品画像にタイトルがオーバーレイしたリストを3カラムで表示させることができます。</p> <pre><code class="javascript">return ( <FlatList style=<span>{</span><span>{</span> flex: 1, paddingTop: 20, backgroundColor: '#dddddd' <span>}</span><span>}</span> contentContainerStyle=<span>{</span><span>{</span> flexDirection: 'row', flexWrap: 'wrap' <span>}</span><span>}</span> data={store.getState().data} renderItem={(rowData)=>{ let image if(rowData.item.node.image !== null){ image = ( <ImageBackground style={ { width: (Dimensions.get('window').width - 20) / 3 - 20, height: 80, marginTop:10, marginBottom:10, marginLeft:10, marginRight:10 } } source={ { uri: rowData.item.node.image.recommendedImageUrl } }> <View style={ { position: 'absolute', //width: '100%', bottom: 0, justifyContent: 'center', alignItems: 'center'<span>}</span><span>}</span>> <Text style=<span>{</span><span>{</span> fontWeight:'bold', backgroundColor: 'rgba(255, 255, 255, 0.8)', width:(Dimensions.get('window').width - 20) / 3 - 20 <span>}</span><span>}</span> numberOfLines={3} ellipsizeMode="tail" >{rowData.item.node.title}</Text> </View> </ImageBackground> ) }else{ image = null } return ( <View style={ { padding: 1, backgroundColor: 'white', margin: 2, width: (Dimensions.get('window').width - 20) / 3, height: 100 } } > {image} </View> ) <span>}</span><span>}</span> /> ) </code></pre> ckoshien