はじめに
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でのフロントエンド開発を加速させる期待の新星なのではと注目しています!
https://expo.dev/changelog/sdk-53#improved-swift-ui-and-jetpack-compose-integration-powering-a-prototype-of-a-new-expo-ui-package
https://docs.expo.dev/versions/latest/sdk/ui/
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
無事にダウンロードができたので、もう一度ビルドを実行します。
!
なぜ npx expo install @expo/ui のあとにビルドが必要なのか?
@expo/ui は、単なるJavaScriptライブラリではなく、iOSやAndroidのネイティブ機能(=OSに組み込まれたUI部品)を内部で使用しており、一部のコンポーネントは、React Native を介してネイティブコードによって描画されるUIコンポーネントです。Expoでは、こうしたネイティブ機能を利用する場合、アプリにそのネイティブコードを組み込んだ上でビルド(再構築)しないと、正しく動作しません。
開発ビルドのセットアップ方法がわからない方
開発ビルドのExpoプロジェクトの立ち上げ方は過去に記事としてまとめているので、わからない方は参考にしてください。
https://zenn.dev/gemcook/articles/7c8298ef7b8d89
実際に触ってみる
実際に全てのコンポーネントを触ってみようと思います!
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でのみ有効)
デザイン 複数のプロパティを組み合わせることで多彩なボタンを実装できます。
コード 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 のみ
デザイン 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] = useState string | 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] = useState Date | 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] = useState number | 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についての発表があるのか注目ですね。
https://x.com/expo/status/1948493075623362616