[React Native]Android、iOSの違いで対応(ドロップダウンリスト)
概要
直近、2ヶ月ぐらいReact Nativeを触っており、その際にハマった点を記事に残します。
この記事では、ドロップダウンリスト実装時、Android(エミュレータ)、iOS(iPhone SE2実機)の動作差異でハマった現象、対応策を記載
フロントエンドエンジニア、モバイルアプリ開発の経験
まず、今回React Nativeを初学しましたが、関連知識経験は以下の通りです。
Vue.js:約4年半
React:実務未経験(今回React Native初学前に、Udemy)
Android、iOS:未経験
実施していた作業
UdemyでRect Nativeの講座を受講後、成果物(メモ帳)に対して機能追加を行い「衝動買いストップアプリ」に改良していました。
機能追加の一つにリストのソート機能があり、そのテスト中に発生。
登録データの一覧リスト上部にUIを配置しており、画面イメージは下記です。
動作環境
-
フレームワーク
- React Native (0.76.6)
- Expo (52.0.30)
- Expo Router (4.0.17)
-
開発ツール
- TypeScript
- Expo Go(iPhone SE2実機でアプリの挙動を確認)
- Android Studio(エミュレータAndroidでアプリの挙動を確認)
現象詳細
ソートUI部分、現象発生時は@react-native-picker/picker
で実装。
Android(エミュレータ)、iOS(Expo Go使用してiPhone SE2実機)で動作行った結果。
下記のように差異がでた。
Android Studioでエミュレータ起動
- UIタップでモーダルが開く
- ソート方式選択でソート処理が実行される
iOS(Expo Go使用してiPhone SE2実機)
- UIタップでモーダルが開かない。
- スワイプするとソート方式の選択がかわる。
iOSでわかりやすく高さ変更した場合
対応策
上記の通りAndroidとiOSでは差異が発生している。
iOSに合わせる場合、UIデザインから変更が必要になる為、タップでモーダルが開く方法を調査した。
結果、stackoverflowで同様の現象について見つかった。
それによりActionSheetIOS
を使用する方式に変更する事とした。
iOS、Androidそれぞれで表示させる為にOS判定処理、ソートキーの定数化など、
リファクタリングも行い対応前後のコード、動作結果は以下。
ソースコード対応前
主要な部分のみ抜粋
詳細はGit参照
return (
<View style={styles.sortContainer}>
<Text style={styles.sortTitleWrap}>並び替え →</Text>
<Picker
selectedValue={pickerValue}
style={styles.picker}
onValueChange={handlePickerChange}
>
<Picker.Item label="最終更新日 新しい順" value='updatedAt:desc' />
<Picker.Item label="最終更新日 古い順" value='updatedAt:asc' />
<Picker.Item label="優先度 高い順" value='priority:asc' />
<Picker.Item label="優先度 低い順" value='priority:desc' />
</Picker>
</View>
)
}
ソースコード対応後
list.tsx
からlistSort.tsx
を参照していた箇所を、listSortAndroid.tsx
、listSortIos.tsx
それぞれOSを判定して表示に変更
主要なJSX部分のみ抜粋
list.tsx
import {
View, StyleSheet, FlatList, Platform,
Text, Alert, ActivityIndicator
} from 'react-native'
<View style={styles.container}>
{/* リストソートUI(ios) */}
{Platform.OS === 'ios' && (
<ListSortIos
itemsSortType={state.sortType}
itemsSortOrder={state.sortOrder}
setItemsSortType={(type) => dispatch({ type: 'SET_SORT_TYPE', payload: type })}
setItemsSortOrder={(order) => dispatch({ type: 'SET_SORT_ORDER', payload: order })}
/>
)}
{/* リストソートUI(android) */}
{Platform.OS === 'android' && (
<ListSortAndroid
itemsSortType={state.sortType}
itemsSortOrder={state.sortOrder}
setItemsSortType={(type) => dispatch({ type: 'SET_SORT_TYPE', payload: type })}
setItemsSortOrder={(order) => dispatch({ type: 'SET_SORT_ORDER', payload: order })}
/>
)}
{/* リスト表示 */}
{buyList(state.items, anonymous)}
<CircleButton onPress={() => handlePress(anonymous)}>
<CustomIcon name="plus" size={40} color={COLORS.white} />
</CircleButton>
</View>
listSortAndroid.tsx
import { View, StyleSheet, Text } from 'react-native'
import { useState } from 'react'
import { type SortType, OrderByDirection, SortValueType } from 'types/list'
import { Picker } from '@react-native-picker/picker'
/**
* ソートオプションキー
*/
type SortOptionKey = `${SortType}:${OrderByDirection}`;
/**
* ソートオプション
*/
const SORT_OPTIONS: Record<SortOptionKey, SortValueType> = {
'updatedAt:desc': '最終更新日 新しい順',
'updatedAt:asc': '最終更新日 古い順',
'priority:asc': '優先度 高い順',
'priority:desc': '優先度 低い順'
}
return (
<View style={styles.sortContainer}>
<Text style={styles.sortTitleWrap}>並び替え → </Text>
<Picker
selectedValue={sortKey}
onValueChange={handlePickerChange}
style={styles.picker}
mode="dropdown"
>
{Object.entries(SORT_OPTIONS).map(([key, value]) => (
<Picker.Item
key={key}
label={value}
value={key}
style={styles.pickerItem}
/>
))}
</Picker>
</View>
)
listSortIos.tsx
import { View, StyleSheet, Text, ActionSheetIOS, TouchableOpacity } from 'react-native'
import { useState } from 'react'
import { type SortType, OrderByDirection, SortValueType } from 'types/list'
/**
* ソートオプションキー
*/
type SortOptionKey = `${SortType}:${OrderByDirection}`;
/**
* ソートオプション
*/
const SORT_OPTIONS: Record<SortOptionKey, SortValueType> = {
'updatedAt:desc': '最終更新日 新しい順',
'updatedAt:asc': '最終更新日 古い順',
'priority:asc': '優先度 高い順',
'priority:desc': '優先度 低い順'
}
// ソートキーとソート順に対応するラベル
const SORT_LABELS = ['キャンセル', ...Object.values(SORT_OPTIONS)]
const showActionSheet = () => {
ActionSheetIOS.showActionSheetWithOptions(
{
options: SORT_LABELS,
cancelButtonIndex: 0
},
handlePickerChange
)
}
return (
<View style={styles.sortContainer}>
<Text style={styles.sortTitleWrap}>並び替え → </Text>
<TouchableOpacity onPress={showActionSheet}>
<Text style={styles.sortTitleWrap}>{SORT_OPTIONS[sortKey]}</Text>
</TouchableOpacity>
</View>
)
Android(Android Studioでエミュレータ起動)
iOS(Expo Go使用してiPhone SE2実機)
まとめ
学習開始前は同一コードでAndroi、iOS両方対応できるのか?など、漠然としたイメージで進めていたが、実際の動作確認で理解した事象である。
iOSでの動作確認もmacOS環境が必須のイメージだったが、途中でExpo Goの存在がわかり所有している実機で確認を進める事ができた。
picker実装時にドキュメントを見ながら進めていたが、把握できていなかった事でありモバイルアプリ開発時の重要ポイント(実機確認)を再確認できた。
参照
「@react-native-picker/pickerドキュメント」
「ActionSheetIOSのドキュメント」
Discussion