1k-ui-recipes について
OneKey の React Native コンポーネントを扱う開発者向けの UI リファレンススキルです。ナビゲーション、スクロール、キーボード、アニメーション、バックグラウンドランタイムの実装指針と対処法をまとめています。
主な機能
- 01iOSタブバーのスクロールオフセット
- 02Webの状態遷移に関する指針
- 03折りたたみ式タブヘッダーのジェスチャー処理
- 04キーボード回避パターン
- 05プラットフォーム固有のナビゲーションとランタイムの修正
活用シーン
- 01キーボードによる入力欄の隠れを防止
- 02iOSオーバーレイ終了時のナビゲーション停止を調査
- 03折りたたみ式タブヘッダー内の横スクロールに対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ onekeyhq/ app-monorepo --skill 1k-ui-recipesインストール後、Claude Code で/1k-ui-recipesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlreferences/rules/android-background-thread-timers-microtasks.mdandroid-bottom-tab-touch-intercept.mdcollapsible-tab-horizontal-scroll.mdios-modal-fabric-frame-animation.mdios-overlay-navigation-freeze.mdios-tab-bar-scroll-offset.mdkeyboard-avoidance.mdstart-view-transition.md詳細
OneKey UI レシピ
よくある UI の問題に対する手軽な解決策です。
クイックリファレンス
| レシピ | ガイド | 主なポイント |
|---|---|---|
| iOS タブバーのスクロールオフセット | ios-tab-bar-scroll-offset.md | iOS のタブページで paddingBottom に useScrollContentTabBarOffset を使用します |
| スムーズな状態遷移 | start-view-transition.md | Web でフェードさせるには、負荷の高い状態更新を startViewTransition で囲みます |
| 折りたたみ可能なタブヘッダー内の横スクロール | collapsible-tab-horizontal-scroll.md | 双方向の Gesture.Pan() と CollapsibleTabContext 経由のプログラムによる scrollTo |
| Android のボトムタブによるタッチの横取り | android-bottom-tab-touch-intercept.md | 一時的な対応 — .android.tsx で GestureDetector と Gesture.Tap() を使用し、ネイティブのタブバーによるタッチの横取りを回避します |
| 入力フィールドのキーボード回避 | keyboard-avoidance.md | KeyboardAwareScrollView による自動スクロール、フッターのアニメーション付きパディング、useKeyboardHeight / useKeyboardEvent フック |
| iOS のオーバーレイナビゲーションのフリーズ | ios-overlay-navigation-freeze.md | オーバーレイを閉じてから遷移するには、順次 goBack() を呼び出すのではなく resetAboveMainRoute() を使用します |
Web での keyboardDismissMode によるタブ間のフォーカス解除 | — | Web では on-drag を絶対に使用しないでください。TextInputState を介して入力フィールドのフォーカスがグローバルに解除されます |
| iOS モーダルの Fabric フレームアニメーション | ios-modal-fabric-frame-animation.md | Fabric の再利用ビューには古いフレームが残ります。モーダル遷移中は updateLayoutMetrics を performWithoutAnimation で囲みます |
| Android バックグラウンドスレッドのタイマーとマイクロタスク | android-background-thread-timers-microtasks.md | Bg Hermes ランタイムには setTimeout がなく、マイクロタスクキューも処理されません。JSI タイマーをインストールし、rt->drainMicrotasks() を呼び出してください(@onekeyfe/react-native-background-thread 3.0.18 で修正済み) |
重要なルールの概要
1. iOS タブバーのスクロールコンテンツオフセット
useScrollContentTabBarOffset を使用して、タブページ内のスクロールコンテナーに動的な paddingBottom を追加します。iOS ではタブバーの高さを返し、それ以外のプラットフォームでは undefined を返します。
import { useScrollContentTabBarOffset } from '@onekeyhq/components';
const tabBarHeight = useScrollContentTabBarOffset();
<ScrollView contentContainerStyle={{ paddingBottom: tabBarHeight }} />
2. startViewTransition によるスムーズな状態遷移
負荷の高い状態更新を startViewTransition で囲みます。View Transition API により Web/デスクトップではフェードし、ネイティブでは setTimeout にフォールバックします。
import { startViewTransition } from '@onekeyhq/components';
startViewTransition(() => {
setIsReady(true);
});
3. 折りたたみ可能なタブヘッダー内の横スクロール(ネイティブ)
折りたたみ可能なタブの renderHeader 内に横スクローラーを配置する場合は、両方の方向を処理する Gesture.Pan() を使用します。水平方向の操作では translateX を更新し、垂直方向の操作では CollapsibleTabContext を介してフォーカス中のタブの ScrollView で scrollTo を呼び出します。
import { CollapsibleTabContext } from '@onekeyhq/components';
react-native-collapsible-tab-view/src/Contextから直接インポートしないでください。必ず@onekeyhq/componentsの再エクスポートを使用してください。
4. Android のボトムタブによるタッチの横取り(一時的な回避策)
一時的な修正 — 根本原因は、タブバーが非表示の場合でも
react-native-bottom-tabsがタッチを横取りすることです。上流の問題が修正されたら、この回避策を削除してください。
Android では、タブバーが GONE の場合でも、react-native-bottom-tabs がタブバー領域のタッチを横取りします。そのため、画面下部付近のボタンがクリックできなくなります。ボタンを GestureDetector と Gesture.Tap() で囲む .android.tsx バリアントを作成して修正します。
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { runOnJS } from 'react-native-reanimated';
const tapGesture = useMemo(
() => Gesture.Tap().onEnd(() => { 'worklet'; runOnJS(onPress)(); }),
[onPress],
);
<GestureDetector gesture={tapGesture}>
<View>
<Button>Label</Button>
</View>
</GestureDetector>
他のプラットフォームに影響しないよう、ファイル拡張子には
.android.tsxを使用してください。
5. 入力フィールドのキーボード回避
標準の Page コンポーネントと Dialog コンポーネントは、キーボード回避を自動的に処理します。手動での処理は、カスタムレイアウトの場合にのみ追加してください。
- Page の入力フィールド:自動 —
PageContainerがKeyboardAwareScrollViewでラップし、bottomOffsetを 90px に設定します - Page フッター:自動 —
useReanimatedKeyboardAnimationによってpaddingBottomをアニメーションします - Dialog:自動 —
showFooter: falseを含むすべてのダイアログで、Dialog レベルでキーボード回避を処理します - カスタムレイアウト:カスタムの
bottomOffsetを指定したKeyboard.AwareScrollViewを使用します
import { Keyboard } from '@onekeyhq/components';
// Custom scrollable area with keyboard avoidance
<Keyboard.AwareScrollView bottomOffset={150}>
{/* inputs */}
</Keyboard.AwareScrollView>
// Dismiss keyboard before navigation
await Keyboard.dismissWithDelay();
カスタム動作向けのフック:
import { useKeyboardHeight, useKeyboardEvent } from '@onekeyhq/components';
const height = useKeyboardHeight(); // 0 when hidden
useKeyboardEvent({
keyboardWillShow: (e) => { /* e.endCoordinates.height */ },
keyboardWillHide: () => { /* ... */ },
});
NavigationContainer の外部にあるコンポーネント(Dialog、Modal)では、
useKeyboardEventWithoutNavigationを使用してください。
6. iOS のオーバーレイナビゲーションのフリーズ(resetAboveMainRoute)
ネイティブの UITabBarController を使用する iOS では、goBack() を連続して呼び出してオーバーレイルート(Modal、FullScreenPush)を閉じると、RNSScreenStack の window-nil 競合状態が発生します。ポップされたページのスクリーンスタックが iOS ウィンドウへの参照を失い、再試行が連続して発生します(50 回、約 100ms 間隔)。その結果、ナビゲーションが約 5 秒間フリーズします。
症状:モーダルを閉じた後、アプリがホームページで止まったように見えます。画面に触れると、ナビゲーションが「動き出します」。
根本原因:react-freeze(NativeTab の freezeOnBlur: true)は、Modal が Main の上にある間、タブのコンテンツを一時停止します。モーダルを閉じる際に、フリーズ解除から Fabric のコミットに至るパイプラインでフラッシュに失敗し、タッチイベントによって React の再評価が強制されるまで、フリーズ前の古いコンテンツが UI に表示され続けることがあります。ネイティブログに表示される RNSScreenStack の再試行(giving up after 50 retries)は、閉じられるモーダルの内部スタックで発生するもので、CPU の負荷にはなりますが、フリーズの原因ではありません。
修正方法:iOS の NativeTab レベルで freezeOnBlur を無効にします(TabStackNavigator.native.tsx)。さらに、オーバーレイからタブへのナビゲーションには switchTab() ではなく switchTabAsync() を使用し、UIKit の遷移が重複しないようにします。
// ❌ WRONG: switchTab overlaps modal dismiss + tab switch
navigation.switchTab(ETabRoutes.Home);
// ✅ CORRECT: switchTabAsync serializes overlay dismiss and tab switch
await navigation.switchTabAsync(ETabRoutes.Home);
主要ファイル:
packages/components/src/layouts/Navigation/Navigator/TabStackNavigator.native.tsx参考資料:調査の全経緯と修正後の根本原因分析については、ios-overlay-navigation-freeze.mdを参照してください。
7. Web:ScrollView の keyboardDismissMode="on-drag" によるタブ間の入力フォーカス解除
Web では、react-native-web の keyboardDismissMode="on-drag" がスクロールイベントのたびに dismissKeyboard() を呼び出します。dismissKeyboard() は TextInputState を使用します。これは、タブごとではなくアプリ全体で現在フォーカスされている入力フィールドを追跡するグローバルシングルトンです。このため、バックグラウンドタブ(例:Home)の ScrollView をスクロールすると、アクティブなタブ(例:Perps)の入力フィールドのフォーカスが解除されます。
症状:ユーザーが操作していないにもかかわらず、入力フィールドのフォーカスが定期的に(約 5 秒ごとに)解除されます。
根本原因の流れ:
- Home タブのカルーセルに
autoPlayInterval={5000}が設定されているため、5 秒ごとにスクロールが発生します - Web の PagerView は
<ScrollView keyboardDismissMode="on-drag">を使用するため、スクロール時にdismissKeyboard()が呼び出されます dismissKeyboard()→TextInputState.blurTextInput(currentlyFocusedField())→ Perps の入力フィールドのフォーカスが解除されます
修正方法(2 つのレイヤー):
Carousel/pager.tsx(Web 専用):keyboardDismissMode="none"を強制します。Web には仮想キーボードがないため、キーボードの非表示処理は副作用しかありませんCarousel/index.tsx:IntersectionObserverを使い、カルーセルがビューポート内に表示されていない場合は自動再生を一時停止します
ルール:
- 絶対に、バックグラウンドタブで動作する可能性のある Web の ScrollView で
keyboardDismissMode="on-drag"を使用しないでください。Web では、TextInputStateを介してフォーカス中の入力フィールドのフォーカスがグローバルに解除されます。 - Carousel/PagerView では、Web 用の
pager.tsxがすでに"none"を強制しています。単独の ScrollView で、モバイルでのみon-dragが必要な場合は、platformEnv.isNativeで囲んでください。 - バックグラウンドのカルーセルの自動再生は、表示されていないときに一時停止してください(
IntersectionObserver)。
// ❌ WRONG: Will blur inputs on other tabs when this ScrollView scrolls
<ScrollView keyboardDismissMode="on-drag" />
// ✅ CORRECT: Only use on-drag on native
<ScrollView keyboardDismissMode={platformEnv.isNative ? 'on-drag' : 'none'} />
主要ファイル:
packages/components/src/composite/Carousel/pager.tsx、packages/components/src/composite/Carousel/index.tsx
8. iOS でのモーダル表示中のコンテンツ位置ずれ(Fabric)
Fabric(新アーキテクチャ)を使用する iOS では、モーダルページ(pageSheet/formSheet)のスライドアップアニメーション中に、コンテンツが誤った位置から飛び込んでくることがあります。原因は、Fabric が古いフレームを保持したネイティブビューを再利用することです。モーダルの遷移中にマウントされると、UIKit はフレームの修正を暗黙的なアニメーションとして扱います。
修正:モーダル遷移中にグローバルな RNSModalTransitionInProgress フラグを使い、Fabric の updateLayoutMetrics と invalidateLayer を UIView.performWithoutAnimation: でラップします。
// In UIView+ComponentViewProtocol.mm updateLayoutMetrics:
extern BOOL RNSModalTransitionInProgress;
if (RNSModalTransitionInProgress) {
[UIView performWithoutAnimation:^{
self.center = CGPoint{CGRectGetMidX(frame), CGRectGetMidY(frame)};
self.bounds = CGRect{CGPointZero, frame.size};
[self layoutIfNeeded];
}];
}
主なファイル:
patches/react-native-screens+4.23.0.patch、patches/react-native+0.81.5.patch参考:ios-modal-fabric-frame-animation.md
9. Android のバックグラウンドスレッドで setTimeout/マイクロタスクキューが機能しない
Android では、@onekeyfe/react-native-background-thread が起動するバックグラウンド JS ランタイムは、独立した Hermes ランタイムです。RN の組み込みタイマーモジュールはメインランタイムにのみ接続されており、バックグラウンドランタイムに処理をディスパッチするカスタム RPC エグゼキューターは、Hermes のマイクロタスクキューを処理しません。その結果、次の 2 つの症状が発生します。
setTimeout/setInterval/requestAnimationFrame/requestIdleCallbackがundefinedになり、await timerUtils.wait(ms)が永久に停止します。Promise.then()/async/awaitの継続処理(await Promise.resolve()も含む)が実行されず、非同期 RPC ハンドラーが最初のawaitで停止し、ブリッジ呼び出しのタイムアウトとして現れます。
修正:@onekeyfe/react-native-background-thread 3.0.18 の android/src/main/cpp/cpp-adapter.cpp で対応済みです。apps/mobile/package.json のバージョンを上げてください。patch-package は不要です。
バックグラウンドランタイムにのみ、次の 2 つの変更を適用します。
- ランタイムをまたぐ各処理の実行後にマイクロタスクを処理する —
nativeExecuteWork内でwork(*rt)の後にrt->drainMicrotasks()を呼び出します。これがPromise.then/async/awaitを機能させる一行です。 - JSI レベルのタイマーをインストールする —
installTimersOnRuntime()により、setTimeout、setInterval、clearTimeout、clearInterval、requestAnimationFrame、cancelAnimationFrame、requestIdleCallback、cancelIdleCallbackを、単一の C++ ワーカースレッドを使うホスト関数として登録します。このスレッドは、SharedRPCが使用するものと同じRPCRuntimeExecutorを介してコールバックをバックグラウンド JS キューにディスパッチします。
// Both fixes are required — they address orthogonal bugs on the same code path.
try { work(*rt); } catch (...) { /* ... */ }
rt->drainMicrotasks(); // ← fixes Promise/async hangs
// In nativeInstallSharedBridge, background branch only:
if (!capturedIsMain) { gBgTimerExecutor = executor; }
SharedRPC::install(*rt, std::move(executor), runtimeId);
if (!capturedIsMain) {
installTimersOnRuntime(*rt); // ← fixes setTimeout/setInterval/rAF/rIC
}
ルール:
polyfillsPlatform.jsにsetTimeoutの JS 側ポリフィルを追加しないでください。この問題は JS レイヤーより下で発生しており、JS ポリフィルを追加してもマイクロタスク処理の欠落が隠れるだけで、修正にはなりません。- 今後、C++ から 3 つ目の JSI ランタイムを生成するコードを追加する場合は、各処理の実行後に
drainMicrotasks()も呼び出す必要があります。呼び出さないと、Promise が停止する症状が再発します。 - iOS には影響しません。
BackgroundRunnerReactNativeDelegateは RN の組み込みタイマーモジュールとデフォルトのマイクロタスク処理を再利用します。
主なファイル:
native-modules/react-native-background-thread/android/src/main/cpp/cpp-adapter.cpp(~/project/app-modulesにある上流版)、apps/mobile/package.json(依存関係を3.0.18に更新) 参考:android-background-thread-timers-microtasks.md
関連スキル
/1k-cross-platform- プラットフォーム固有の開発/1k-performance- パフォーマンスの最適化/1k-coding-patterns- 一般的なコーディングパターン