はじめに
2025年4月にExpo SDK53がリリースされたのですが、公式が発表したChangelogに興味深い記述がありました。
Improved Swift UI and Jetpack Compose integration, powering a prototype of a new Expo UI package
どうやら、Expoが Swift UI と Jetpac Compose を統合することでネイティブに近いUIコンポーネントを開発者が簡単に使える新たなパッケージ、 Expo UI のプロトタイプを公開したようです。
この Expo UI ですが、Expoでのフロントエンド開発を加速させる期待の新星なのではと注目しています!
Expo UIが期待の新星である理由
なぜ Expo UI に期待を寄せているのかと言うと、現在のExpoでのフロントエンド開発において必須である「UIライブラリの選定」という面倒な工程を省くことができるのではないかと考えているからです。
Expo UI が有用ライブラリだと…
現状のExpoフロントエンド開発においてボタンやピッカーなどのUIライブラリを使いたい場合、様々な選択肢がある中、開発者自身でライブラリの良し悪しを判断して選定する必要があります。
ですが、もしExpo UI が有用なライブラリであった場合、公式が提供・メンテナンスしている Expo UI を利用することで、ライブラリ選定に悩む必要がなくなるという大きなメリットがあります。
Expo UI の特徴
実際に触っていく前に、Expo UI の特徴について調べてみました。
1. 公式メンテナンスの信頼性
Expoチームが直接メンテナンスをしているため、他のサードパーティ製UIライブラリに比べて以下のような信頼性が担保されています。
- Expoとの互換性が高い
- 将来的なアップデート対応もスムーズ
- バグ対応・セキュリティ面でも安心感⭕️
2. ネイティブ描画による高パフォーマンス
内部的にはiOSでは SwiftUI 、Androidでは Jetpack Compose を使ってネイティブUIを描画しているので、従来のReact Nativeコンポーネントよりスムーズかつ高速な動作が期待できます。
3. クロスプラットフォーム対応
Expo UIのコンポーネントはiOSとAndroidで同一のコードで動作するように設計されているため、クロスプラットフォームに対応しているアプリ開発のコスト削減が可能です。
4. Expo Go では使用できない
Expo UI はネイティブコード(SwiftUI や Jetpack Compose)を使っているため、ネイティブコードを含むパッケージを利用できないExpo Goでは使用することができません。
Expo UI を試してみたい方は、Expo Go ではなく、EAS を利用してカスタムビルドを作成する必要があります。
5. 現在はアルファ版のプロトタイプ段階
Expo UI はまだ試験的機能として提供されているため、本番アプリに利用する場合は慎重な検討を行ってください。とはいえ、今後Expo標準UIになる可能性も十分にあるため、今のうちに触っておく価値は高いと考えます。
Expo UI をインストールする
特徴について理解したところで、早速 Expo UI をダウンロードしていきます。
npx expo install @expo/ui
無事にダウンロードができたので、もう一度ビルドを実行します。
開発ビルドのセットアップ方法がわからない方
開発ビルドのExpoプロジェクトの立ち上げ方は過去に記事としてまとめているので、わからない方は参考にしてください。
実際に触ってみる
実際に全てのコンポーネントを触ってみようと思います!
BottomSheet
デザイン
シンプルなデザインで、汎用性が高そうです。内部の実装に合わせて高さが変わります。
動きは滑らかで、背景をクリックするかボトムシートを下にスライドすることで閉じることができます。
コード
開閉状態を管理するためにuseState
と併用するケースが多そうです。
import { BottomSheet } from '@expo/ui/swift-ui';
import { useState } from 'react';
const [isOpened, setIsOpened] = useState(false);
>
View>
Button title='Open Bottom Sheet' onPress={() => setIsOpened(true)}/>
/View>
BottomSheet isOpened={isOpened} onIsOpenedChange={setIsOpened}>
Text>
Hello, world!
/Text>
/BottomSheet>
/>
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
children |
any |
⭕️ | BottomSheet内に表示するコンテンツ。 |
isOpened |
boolean |
⭕️ | BottomSheetが開かれているかどうかを示すブール値。 |
onIsOpenedChange |
(isOpened: boolean) => void |
⭕️ | BottomSheetの開閉状態が変更されたときに呼び出されるコールバック関数。 |
TextInput
デザインkeyboardType
を指定することで、フォーカスを当てた時に開くキーボードの種類を制御することができます。
Basic & Phone & Email
URL & Twitter/Social & Web
改行を無限に許容することと、最大行数を指定して改行を許容することもできます。
また、英語のみのようですが、タイプミスを補完する機能を任意でオフにすることができます。
コードonFocus
やonBlur
などのサブスクライブ系のプロパティは無いようです。
import { TextInput } from '@expo/ui/swift-ui';
import { useState } from 'react';
const [value, setValue] = useState('A single line text input');
TextInput autocorrection={false} defaultValue="A single line text input" onChangeText={setValue} />
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
defaultValue |
string |
任意 | 初期表示されるテキスト(コンポーネントがアンマウントされるまで保持) |
onChangeText |
(value: string) => void |
⭕️ | 入力テキストが変更されたときに呼ばれる関数 |
multiline |
boolean |
任意 |
true にすると複数行の入力が可能になる(改行キーはなし) |
numberOfLines |
number |
任意 |
multiline が有効な場合の表示行数(超えるとスクロール) |
keyboardType |
キーボードタイプ(下記参照) | 任意 | 表示するキーボードの種類(例: 'default' , 'numeric' , 'email-address' など) |
autocorrection |
boolean |
任意 | 自動補正の有効/無効(デフォルトは true ) |
style |
StyleProp |
任意 |
ボタンの外側ラッパーである Host` コンポーネントのスタイル |
keyboardTypeの値一覧
値 | 説明 |
---|---|
'default' |
標準のキーボード |
'email-address' |
メールアドレス入力向けキーボード(@ や . を含む) |
'numeric' |
数字のみのキーボード(整数) |
'phone-pad' |
電話番号用の数字キーボード |
'ascii-capable' |
ASCII文字のみ入力可能なキーボード |
'numbers-and-punctuation' |
数字と句読点を含むキーボード |
'url' |
URL入力に適したキーボード(.com や / 含む) |
'name-phone-pad' |
名前・電話番号入力向けのキーボード |
'decimal-pad' |
小数点付きの数値入力用キーボード |
'twitter' |
Twitter向け入力キーボード(@ や # を含む) |
'web-search' |
検索向けキーボード(検索ボタン付き) |
'ascii-capable-number-pad' |
ASCII準拠の数字パッド(iOSでのみ有効) |
Button
デザイン
複数のプロパティを組み合わせることで多彩なボタンを実装できます。
コード
react-native製のボタンコンポーネントとはtitle
プロパティで表示する文字を指定しますが、Expo UI はCildrenで表示内容を指定します。
import { Button } from '@expo/ui/swift-ui';
Button
style={{ flex: 1 }}
onPress={() => {
console.log('clicked');
}}>
Click me
/Button>
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
children |
string or React.ReactNode
|
⭕️ | ボタンに表示されるテキストまたはノード |
onPress |
() => void |
任意 | ボタンが押されたときに呼ばれる関数 |
systemImage |
string |
任意 | Material Icons(Android)やSF Symbols(iOS)の名前 |
role |
'default' | 'cancel' | 'destructive'
|
任意(iOSのみ) | iOSでの意味づけ(警告・キャンセルなど) |
variant |
ボタンスタイル(下記参照) | 任意 | OSごとのビジュアルスタイル |
color |
string (カラーコード) |
任意 | ボタンの色(red やblue でも可) |
disabled |
boolean |
任意 |
true にするとボタンが無効化されて押せなくなる |
style |
StyleProp |
任意 | ボタンの外側ラッパーである Host コンポーネントのスタイル |
**variant**の値一覧
値(文字列) | 説明 | プラットフォーム |
---|---|---|
'default' |
デフォルトのシステムボタンスタイル | iOS / Android 共通 |
'bordered' |
枠線がある薄い背景のボタン。Android では FilledTonalButton に相当 |
iOS / Android 共通 |
'plain' |
枠や背景なしのシンプルなボタン | iOS / Android 共通 |
'borderedProminent' |
強調された外観の枠線付きボタン(太字・濃い色など) | iOS のみ |
'borderless' |
背景も枠もない透過ボタン(=タップ領域だけ) | iOS のみ |
'accessoryBar' |
アクセサリバー(入力補助バー)用のスタイル | macOS のみ |
'accessoryBarAction' |
アクセサリバー内での操作ボタンスタイル | macOS のみ |
'card' |
カードUIに適したボタンスタイル | macOS のみ |
'link' |
リンク風の見た目のボタン | macOS のみ |
Switch (toggle/checkbox/button)
デザイン
iPhonの設定画面にあるToggleSwitchそのまんまで、動きも非常にスムーズです。variant
プロパティを変えることでCheckboxとButtonにもなります。
コード
現状の公式ページでは開閉のbool値を渡すのはchecked
プロパティと書いていますが、実装はvalue
プロパティに渡す設計になっているようです。
import { useState } from 'react';
import { Switch } from '@expo/ui/swift-ui';
const [isChecked, setIsChecked] = useState(false);
Switch
value={isChecked}
onValueChange={setIsChecked}
label="通知を許可"
variant='switch'
/>
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
value |
boolean |
⭕️ | スイッチのオン/オフ状態 |
onValueChange |
(value: boolean) => void |
任意 | 状態が変更されたときのコールバック |
label |
string |
任意 | スイッチやボタンの隣や中に表示するラベル |
variant |
'switch' | 'checkbox' | 'button'
|
任意 | 表示形式を指定(省略時はswitch ) |
color |
string |
任意 | スイッチがオンのときの色(デフォルトはiOSグリーン) |
elementColors |
undefined |
任意 |
button バリアントでは指定できないようになっている |
style |
StyleProp |
任意 | ボタンの外側ラッパーである Host コンポーネントのスタイル) |
ColorPicker
デザイン
ネイティブのカラーピッカーそのままなので、使い勝手は非常に良さそうです。
コード
styleプロパティを指定できますが、現状ではwidth
,height
を指定しても特に反映されませんでした。
import { useState } from 'react';
import { ColorPicker } from '@expo/ui/swift-ui';
const [color, setColor] = useStatestring | null>('#FF0000');
ColorPicker
style={{ width: 400, height: 200 }}
selection={color}
onValueChanged={setColor}
label='色を選んでください'
/>
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
selection |
string | null |
⭕️ | 現在選択されている色。'#RRGGBB' または '#RRGGBBAA' 形式で指定。 |
label |
string |
任意 | カラーピッカーの上に表示されるラベル |
onValueChanged |
(value: string) => void |
任意 | 色が変更されたときに呼ばれるコールバック関数 |
supportsOpacity |
boolean |
任意 | 透過度(アルファチャンネル)を選択可能にするかどうか |
style |
StyleProp |
任意 | ボタンの外側ラッパーである Host コンポーネントのスタイル |
DateTimePiker (date/time)
デザイン
シンプルながらも幅広い機能性を持っていて、いろんな場面で使えそうです。
DatePicker
TimePicker & DateTimePicker
コードdisplayedComponents
を'date'
にするとDatePicker、'time’
にするとTimePiker、'dateAndTime'
にするとDateTimePickerになります。
import { DateTimePicker } from '@expo/ui/swift-ui';
import { useState } from 'react';
const [selectedDate, setSelectedDate] = useStateDate | null>(null);
DateTimePicker
initialDate={selectedDate.toISOString()}
onDateSelected={date => {
setSelectedDate(date);
}}
title='日付を選んでください'
displayedComponents='date'
variant='wheel'
/>
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
initialDate |
string | null |
任意 | 初期表示する日付(例:’2025-01-01’) |
title |
string |
任意 | iOS でピッカーの上に表示されるタイトル |
onDateSelected |
(date: Date) => void |
任意 | 日付または時間が選択された時に呼ばれるコールバック |
variant |
'wheel' | 'automatic' | 'graphical' | 'compact' |
任意(デフォルト 'automatic' ) |
ピッカーの表示スタイル(iOS) |
displayedComponents |
'date' | 'hourAndMinute' | 'dateAndTime' |
任意(デフォルト 'date' ) |
ピッカーに表示する項目。日付のみ・時間のみ・両方から選べる |
color |
string |
任意 | ピッカーのアクセントカラー(iOS用) |
style |
StyleProp |
任意 | ボタンの外側ラッパーである Host コンポーネントのスタイル |
Picker
デザイン
個人的にはSegmented
タイプとmenu
タイプのピッカーが使いやすそうです。inline
タイプのピッカーは実装方法がわからなかったので省いています。
コード
現時点だけかもしれませんが、Wheel
タイプのピッカーの場合は
コンポーネントでラップしないとうまく描画されませんでした。
import React, { useState } from 'react';
import { Picker } from '@expo/ui/swift-ui';
const [selectedIndex, setSelectedIndex] = useStatenumber | null>(0);
Picker
options={['$', '$$', '$$$', '$$$$']}
selectedIndex={selectedIndex}
onOptionSelected={({ nativeEvent: { index } }) => {
setSelectedIndex(index);
}}
variant="segmented"
/>
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
options |
string[] |
⭕️ | 選択肢リスト |
selectedIndex |
number | null |
⭕️ | 現在選択されているインデックス |
label |
string |
任意 |
menu variant 時にフォームに表示されるラベル(iOSのみ) |
onOptionSelected |
(event) => void |
任意 | 選択変更時のコールバック。indexとlabelを取得可能 |
variant |
'wheel' | 'segmented' | 'menu' | 'inline' | 'palette' |
任意 | 表示スタイル(デフォルト: 'segmented' ) |
color |
string |
任意 | iOSの menu variant で使用される色 |
CiclarProgress/LinearProgress
デザイン
どちらも進捗表示系のコンポーネントで、円型と直線型です。
CiclarProgressの方はなぜかローディングスピナーが表示されてしまいますが、、、
コード
使い方は非常に簡単で、どちらも一緒です。
import { CircularProgress, LinearProgress } from '@expo/ui/swift-ui';
CircularProgress progress={0.6} color="blue" />
LinearProgress progress={0.3} color="green" />
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
progress |
number | null |
任意 | プログレスの値 |
color |
ColorValue |
任意 | プログレスの色('red' 、'#FF0' など) |
Gauge
デザインCiclarProgress
/LinearProgress
と似ていますが、こちらの方は温度や容量の用途に向いていてグラデーションカラーなども実装できます。
コードcolor
プロパティに色の配列を渡すとグラデーションになります。
import { Gauge } from "@expo/ui/swift-ui";
Gauge
max={{ value: 100, label: '100%' }}
min={{ value: 0, label: '0%' }}
current={{ value: 50 }}
color={["blue", "green", "yellow", "orange", "red"]}
type="circularCapacity"
/>
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
label |
string |
任意 | ゲージの上などに表示するテキスト |
labelColor |
ColorValue |
任意 | ラベルの色 |
current |
ValueOptions |
⭕️ | 現在の値。value (数値)と、必要ならlabel , color を含む |
min |
ValueOptions |
任意 | 最小値の設定(ラベルや色も指定可能) |
max |
ValueOptions |
任意 | 最大値の設定(ラベルや色も指定可能) |
type |
'default' | 'circular' | 'circularCapacity' | 'linear' | 'linearCapacity' |
任意 | ゲージのスタイルタイプ(iOS専用) |
color |
ColorValue or ColorValue[]
|
任意 | ゲージの色。グラデーションの場合は配列で指定可能 |
style |
StyleProp |
任意 | ボタンの外側ラッパーである Host コンポーネントのスタイル |
Slider
デザイン
非常にシンプルなスライダーです。色を指定することができます。
コードmin
とmax
を指定してない場合は 0 ~ 1 のスライダーになります。
import { Slider } from '@expo/ui/swift-ui';
import { useState } from 'react';
const [value, setValue] = useState(0.3);
Slider
style={{ minHeight: 60 }}
value={value}
onValueChange={(value) => {
setValue(value);
}}
/>
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
value |
number |
任意 | 現在のスライダーの値(デフォルト: 0 ) |
steps |
number |
任意 | ステップ数。0だと無限ステップ(連続的なスライド)(デフォルト: 0 ) |
min |
number |
任意 | スライダーの最小値(デフォルト: 0 ) |
max |
number |
任意 | スライダーの最大値(デフォルト: 1 ) |
color |
string |
任意 | スライダーの色 |
onValueChange |
(value: number) => void |
任意 | 値が変化したときに呼ばれるコールバック |
style |
StyleProp |
任意 | ボタンの外側ラッパーである Host コンポーネントのスタイル |
List
デザイン
設定画面でよくみるリストに非常に近く、選択できるリストや並び替え・削除ができるリストなどもあります。現時点ではまだアルファ版なので仕方がないのですが、挙動やレンダリングが不安定です。
コード
childrenを渡すだけでも機能しますが、複数のプロパティを組み合わせることも可能です。
import { Text } from 'react-native';
import { List } from '@expo/ui/swift-ui/List';
List
listStyle="insetGrouped"
deleteEnabled
onDeleteItem={(index) => console.log("削除:", index)}
>
Text>機内モード/Text>
Text>Wi-Fi/Text>
Text>Bluetooth/Text>
/List>
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
children |
React.ReactNode |
⭕️ | リスト内に表示するコンテンツ |
listStyle |
"automatic" | "plain" | "inset" | "insetGrouped" | "grouped" | "sidebar" |
任意 | SwiftUIのListStyle を指定 |
selectEnabled |
boolean |
任意 | 項目の選択を有効にする |
moveEnabled |
boolean |
任意 | 項目のドラッグによる並び替えを有効にする |
deleteEnabled |
boolean |
任意 | 項目のスワイプ削除を有効にする |
scrollEnabled |
boolean |
任意 | リストのスクロールを有効にする(iOS 16+) |
editModeEnabled |
boolean |
任意 | SwiftUIの編集モードを有効にする |
onDeleteItem |
(index: number) => void |
任意 | 項目が削除されたときのコールバック |
onMoveItem |
(from: number, to: number) => void |
任意 | 項目が並び替えられたときのコールバック |
onSelectionChange |
(selection: number[]) => void |
任意 | 項目の選択状態が変化したときのコールバック |
style |
StyleProp |
任意 | ボタンの外側ラッパーである Host コンポーネントのスタイル |
デザイン
トリガーに指定した要素をタップ/長押しすることでメニューが表示されます。
メニューにはButton
の他に、上記で紹介したSwitch
やPicker
(menu)を埋め込むことも可能です。
Single Press & Long Press
Switch & Picker(close) & Picker(open)
LongPressの場合はプレビューを設定できます。
また、トリガーとプレビューはどちらも写真にすることも可能です。
コード
複数のコンポーネントがあるのでそれぞれご紹介します。
-
ContextMenu
- メニュー全体を管理するコンポーネント
-
ContextMenu.Trigger
- メニューを起動するタップ/長押し要素。例えば画像やカードなどの任意のViewを包む
-
ContextMenu.Items
- Triggerで表示されるメニューアイテム
-
Button
,Switch
,Picker
,Submenu
などの要素をここに並べる
-
ContextMenu.Preview
(iOS専用)- メニュー起動時に上に表示される内容 (画像のプレビューなど)
import { ContextMenu ,Button } from '@expo/ui/swift-ui';
ContextMenu activationMethod="longPress">
ContextMenu.Trigger>
Text>メニューを開くには長押し/Text>
/ContextMenu.Trigger>
ContextMenu.Items>
Button text="menu1" onPress={() => console.log('menu1')} />
Button text="menu2" onPress={() => console.log('menu2')} />
/ContextMenu.Items>
ContextMenu.Preview>
Image source={source}/>
/ContextMenu.Preview>
/ContextMenu>
指定できるプロパティは以下の通りです。
プロパティ名 | 型 | 必須 | 説明 |
---|---|---|---|
activationMethod |
"singlePress" | "longPress" |
任意 | メニューの起動方法。シングルタップまたは長押し。 |
children |
ReactNode |
⭕️ | トリガー要素や コンポーネントなど。 |
style |
StyleProp |
任意 |
ボタンの外側ラッパーである Host` コンポーネントのスタイル |
-
activationMethod
("singlePress" | "longPress"
/ 任意)
メニューの起動方法。シングルタップまたは長押し。 -
children
(ReactNode
/ ⭕️必須)
トリガー要素や
コンポーネントなど。 -
style
(StyleProp
/ 任意)
ボタンの外側ラッパーであるHost
コンポーネントのスタイル
まとめ
アルファ版なので不安定なところもありますが、すでに完成度の高いコンポーネントも複数あり、非常に高いポテンシャルを感じました。Expoの公式XによるとExpo SDK 54のリリースは8月末予定のようなので、Expo UIについての発表があるのか注目ですね。
Views: 0