Expo 在 iPhone Duo 上
來源:https://expo.dev/blog/supporting-iphone-duo (Expo SDK 58)。設計規則位於 apple-design-duo;此技能是 Expo 與 React Native 的實作層。CI 管線背景知識:https://docs.expo.dev/tutorial/cicd/introduction/ 及 expo:eas-workflows 技能。
需求
- Expo SDK 58(目標為 iOS 27)與 Xcode 27.1(iOS 27.1 執行環境加上 iPhone Duo 模擬器)。
- EAS Build 映像檔:
sdk-58或latest。 - Xcode 27 將 Simulator.app 取代為 Device Hub(Xcode > Open Developer Tool > Device Hub)。它有一個可摺疊、旋轉並設定鉸鏈角度的 3D Duo。
心智模型
摺疊 = 緊湊寬度,展開 = 一般寬度。讓版面變寬,但絕不切換設計。雙面保持相同的功能與狀態。在書本般的姿態下,iOS 會將狀態列、分頁列與工具列移至一邊的垂直邊欄中。
四條規則
- 讓 iOS 繪製導航列。 使用 Expo Router 的
Stack與NativeTabs。自訂的 JS 標題與<TabList>不支援邊欄(SDK 58 僅加入最低限度的相容性),而且<TabList>無法放入邊欄。 - 每個畫面都使用
SafeAreaView(react-native-safe-area-context),包含水平內邊距。 - 測試每一種姿態。 摺疊、展開、旋轉、分割顯示。
- 只在必要處調整:
ArrangementView、preferredPlacement、保留區域、鉸鏈 API,或是停用邊欄。
安全區域(大多數錯誤所在)
- 邊欄需要左側或右側 84pt 的內邊距。
- 將捲動內容包在
SafeAreaView edges={['left','right']}中,而非ScrollView,這樣背景才能延伸至邊緣。在 ScrollView 上設定contentInsetAdjustmentBehavior="automatic"(RN patch #59194 應可讓此設定單獨生效)。 - 水平輪播:自己加入結尾內邊距。
- 浮動按鈕與提示:
right: insets.right + 16。在 RTL 模式下,需同時讀取left與right。 - 在
NativeTabs內部,只有當分頁列位於底部時,insets.bottom才會包含分頁列的高度。絕不要使用固定的分頁列高度。 - 為內容容器加入內邊距,而非根視圖。
- 在渲染時讀取
useWindowDimensions。左側分割視窗的寬度為 469pt。
Expo Router
- 邊欄中的標題按鈕以單欄方式堆疊,返回或關閉按鈕在前。純文字項目保持水平。自訂視圖則被排除。
- Dynamic Island 中的即時活動可能會將工具列縮小為溢位選單。不要與之對抗。
<SplitView>包裝了系統分割視窗:摺疊時單欄、展開時雙欄,且感知摺疊狀態。在 SDK 58 中為實驗性功能。NativeTabs的preferredPlacement:'sidebar' | 'tabBar' | 'automatic'。在橫向的展開顯示器上套用。
額外 API
| 需求 | API |
|---|---|
| 雙窗格內容(僅 iOS 27.1,較舊版本會以無容器方式渲染子元素) | 來自 @expo/ui/swift-ui 的 ArrangementView,置於 Host 內,並使用 ArrangementView.Primary 與 .Secondary。使用 `arrangementViewStyle('split' |
| 摺疊與遮擋框架 | npx expo install react-native-reserved-regions,然後 useReservedRegions() 會回傳 `{ kind: 'division' |
| 鉸鏈角度與狀態 | npx expo install expo-hinge。useHinge()、getHinge()、addHingeListener()。狀態:closed、partiallyOpen、fullyOpen、unknown。SwiftUI 修飾符 onHingeChange 可透過 useWorkletCallback 在 UI 執行緒上執行。可與 Reanimated 及 Android 11+ 摺疊裝置搭配使用。 |
| 邊欄邊緣 | useVerticalBarEdgeIOS() 會回傳 'leading'、'trailing' 或 null。並非 Duo 偵測器(展開直向時回傳 null)。 |
| 停用邊欄 | <StatusBar verticalBarBehaviorIOS="disabled" />(對應到 preferredVerticalBarBehavior)。僅用於影片播放器或計算機風格的畫面,且需維持每個畫面的選擇穩定。 |
方向
在 iOS 27 上,方向鎖定在可調整大小的上下文中是偏好設定。lockAsync 有可能被拒絕,且 requireFullScreen 不再能選擇退出。在 Duo 上鎖定為直向或橫向僅在應用程式隨著姿態變化填滿螢幕時才有效。
已知限制
- Duo 模擬器中無法使用 StandBy,且 Xcode 27.1 的模擬器中無法使用大多數應用程式擴展。請在實體裝置上測試 Widget 與擴展。
expo-app-intents(alpha 版,同樣是 SDK 58)可將動作暴露給 Siri、Shortcuts 與 Spotlight。某些架構所需的 iOS 版本比其所屬領域更高(.mail.openDraft需要 iOS 27)。與版面配置無關。
測試檢查清單
- 摺疊: 邊欄下方無重要元素,按鈕可正常回應。
- 展開: 版面未切換設計,邊欄下方無內容,直向時狀態列回到頂部與底部。
- 部分展開: 摺疊處無重要元素,工作表與彈出視窗落在單側。
- 分割顯示: 內容與邊欄保持距離(左側視窗的左邊緣)。
- 螢幕中間摺疊: 捲動位置、狀態與動作持續保留。
- 標準 iPhone: 沒有任何改變。
稽核工作流程
- 確認 SDK 58 並重新建置。
- 搜尋自訂標題或分頁列(
headerShown: false加上自訂列、TabList),並遷移至Stack與NativeTabs。 - 搜尋硬編碼的
top、bottom、left、right偏移量、固定的分頁列高度,以及根層級的內邊距。改用內邊距。 - 根據安全區域清單檢查輪播、FAB 與提示。
- 選擇雙窗格畫面:
SplitView或ArrangementView。 - 僅在自訂版面確有需要時,決定每個畫面的保留區域或鉸鏈。
- 先在 Device Hub 中驗證,然後在 CI 中驗證(如下)。
CI:在 Duo 模擬器上執行
使用 expo:eas-workflows 技能(先取得最新的 schema,不要猜測 job 參數)。一個合理的形狀來自 CI/CD 教學:PR 觸發的模擬器建置,使用 sdk-58 或 latest 映像檔,可選的 Maestro E2E,然後根據指紋在原生建置與用於生產環境的 EAS Update 之間選擇。使用 npx -y eas-cli@latest workflow:validate .eas/workflows/<file>.yml --non-interactive 進行驗證。Workflows 會消耗付費的 EAS 分鐘數。
常見錯誤
| 錯誤 | 修正方法 |
|---|---|
| 為 ScrollView 加入內邊距 | 用 SafeAreaView 包裹,並設定 contentInsetAdjustmentBehavior="automatic" |
| 在內邊距中使用固定的分頁列高度 | 使用安全區域的 insets.bottom |
使用 useVerticalBarEdgeIOS() 來偵測 Duo | 它只代表邊欄邊緣,在展開直向時為 null |
| 使用保留區域在摺疊處分割內容 | 改用 ArrangementView 或 SplitView |
| 自建的 JS 標題或分頁列 | 改用 Stack 與 NativeTabs |
浮動按鈕設為 right: 16 | 改為 right: insets.right + 16 |
假設 lockAsync 總是成功 | 將其視為偏好設定,並讓內容填滿螢幕 |
| 只在標準 iPhone 上測試 | 使用 Device Hub,測試每一種姿態 |