為 iPhone Duo 準備您的 App:一個完整的實作範例
要怎麼為 iPhone Duo 準備 App? 用 iOS 27.1 SDK 建置,在模擬器裡讓它走過每一種姿態,修好走查發現的問題,再圍繞 Apple 尚未公布的兩個日期安排送審。iPhone Duo 將於 10 月 23 日開賣,搭載 iOS 27.1。1 App 在這支手機上能拿到什麼,取決於二進位檔裡標記的 SDK 版本:同一個原始檔分別以 26.0、27.0 與 27.1 連結後,得到的是一個手機大小的方框、一個緊鄰狀態直欄的視窗,以及整塊螢幕,後者的工具列與標籤列沿側邊排列,摺疊處也有回報。12 截至 10 月 2 日,只有 Apple 的 beta 會標記 27.1 或更新的版本(帶有 Duo 模擬器的 Xcode 27.1 beta,以及 Xcode 27.2 beta),而 App Store Connect 接受它們的建置用於 TestFlight,不接受用於上架。8 本文以 Kiradex 完整走一遍:這是一款我們放在 TestFlight 上的卡牌收藏 App。內容包括哪些東西不必動手就到位、走查抓到了哪些光讀程式碼沒看出的問題、兩塊螢幕的截圖,以及一份可以交給程式碼代理的工作說明。
重點摘要
- SDK 標記就是開關。 iOS 讀的是二進位檔
LC_BUILD_VERSION裡的 SDK 版本。標記為 27.1 時,探測 App 拿到整塊螢幕,闔上時 466 × 678 點、展開時 951 × 669 點,並有垂直列與保留區域。標記為 27.0 時,它在狀態直欄所在的邊緣前 80 點就停住,工具列維持橫向,對摺疊處一無所知。標記為 26.0 時,它是一支裝在方框裡、375 × 667 的手機。請用otool -l檢查您的建置。12 - 時程上有兩個未定的日期。 TestFlight 自 9 月 18 日起接受 27.1 SDK 的建置,自 9 月 16 日起接受 27.2 beta 的建置;App Store 只接受 27.0 SDK 的建置;Duo 的截圖尺寸已經公布,上傳功能則「will be available later this year」(將於今年稍晚開放)。89 上傳時現在必須附上啟動畫面的設定鍵。10
- 大部分工作由系統容器完成。 一個
TabView、五個分頁中四個放著NavigationStack,再加上同時具備標題與符號的工具列項目,就讓 Kiradex 的各種列移到側邊,完全不需要 Duo 專屬程式碼。一個ArrangementView把展開的螢幕一分為二,並在 Book 姿態下把分隔線移到摺疊處。一個onHingeChange在手機展開時重播 App 的開場動畫。13 - 走查抓到了光讀程式碼沒抓到的問題。 只有一個文字「Done」按鈕的 sheet 會保留側欄卻讓它空著。全螢幕卡片會跑到外側相機底下。轉成直向後,分割畫面上下堆疊。闔上時打開、展開手機後仍停留在畫面上的卡片,會落進 compact 版面,被拉寬。為內側螢幕寫的 regular 寬度版面,在橫放的 6.9 吋 iPhone 上會跑版。13
- 在商店接受 27.1 之前,同一份原始碼需要兩個 Xcode。
#available幫不上忙;27.0 SDK 裡根本沒有ArrangementView可供編譯。用一個編譯條件,或用#if canImport(SwiftUI, _version: 8.0.85),把新呼叫圍起來。14 - 截圖出自同一次執行。 模擬器的擷取圖正好是 App Store Connect 要的尺寸,Apple 的 Duo 外框圖中的開口也是。Apple 的規則仍然要求正面、不加修改,也不得使用裝置的 3D 算圖。911
10 月 2 日的現況
| 狀態 | 日期 | |
|---|---|---|
| 手機 | 10 月 16 日開放預購,10 月 23 日開賣,「available with iOS 27.1」(搭載 iOS 27.1 推出)1 | 9 月 9 日 |
| SDK | Xcode 27.1 beta(27A9269)帶有 iOS 27.1 SDK 與 iPhone Duo 模擬器;Apple 的發佈摘要沒有列出第二個 27.1 beta,也沒有候選發行版。Xcode 27.2 beta 帶有相同的 API,但沒有 Duo 模擬器814 | 9 月 18 日;摘要於 10 月 2 日讀取 |
| TestFlight | 開放 Xcode 27.1 beta 與 Xcode 27.2 各 beta 的建置,內部與外部測試皆可8 | 9 月 16 日、18 日與 28 日 |
| App Store | 開放 Xcode 27 與 27.0 SDK 的建置。沒有任何一則公告向 27.1 SDK 開放8 | 9 月 14 日 |
| Duo 截圖 | 尺寸已公布;上傳「will be available later this year」(將於今年稍晚開放)9 | 9 月 9 日 |
| 啟動畫面 | 凡是以 iOS 27 SDK 或更新版本建置的 App,上傳時都必須具備10 | 技術說明於 9 月 14 日修訂 |
其中三列在等 Apple,但沒有一列會擋住工作。從這張表推出來的順序是:讓 App 改用 27.1 SDK 並跑進模擬器,讓它走過每一種姿態,修好走查發現的問題,把那個建置放上 TestFlight,再把上架送審與 Duo 截圖準備好,等各自開放的那一天。
Apple 自己的起點是六場 Tech Talk 中的第一場,長十分鐘,以下內容都假設您已看過:
第一步:SDK 標記決定手機給您什麼
這場演講一開頭就給出分成三級的相容性承諾。沒有用 iOS 27 SDK 建置的 App 仍然能執行:「When the device is closed, your app will use the screen space to the left of the status bar and camera. When the device is open, your app will be a familiar size and aspect ratio.」(裝置闔上時,App 使用狀態列與相機左側的螢幕空間;裝置展開時,App 會是熟悉的尺寸與長寬比。)(0:36) 採用 iOS 27 SDK 的 App 則「will extend to the left of the status bar area on the inner display.」(在內側螢幕上會延伸到狀態列區域的左側。)(0:58) 接著是:「When you build your app with the iOS 27.1 SDK, your app extends to the edge of the screen. Standard navigation and toolbar buttons now lay out vertically under the status bar.」(以 iOS 27.1 SDK 建置時,App 會延伸到螢幕邊緣,標準的導覽與工具列按鈕改為在狀態列下方垂直排列。)(1:05)3
這三個等級不是您程式碼的屬性,而是二進位檔裡一個數字的屬性:連結器記錄在 LC_BUILD_VERSION 載入指令中的 SDK 版本,iOS 在啟動時讀取它。為了看看這個數字有多大的影響,我寫了一支小小的探測 App:一個 TabView,裡面是帶有五個工具列項目的 NavigationStack,再加上一個 ArrangementView,並從同一個原始檔建置了三次。三個二進位檔唯一的差別就是那個數字:26.0、27.0、27.1。接著我讓三者都在 iPhone Duo 模擬器上走過每一種姿態。12

同一個原始檔,三種 SDK 標記,展開且直立。由左至右:26.0、27.0、27.1。
| 姿態 | 標記 26.0 | 標記 27.0 | 標記 27.1 |
|---|---|---|---|
| 闔上 | 375 × 667,縮放後放進直欄旁的空間 | 386 × 678,位於直欄旁 | 466 × 678,整塊螢幕 |
| 展開 | 375 × 667,螢幕正中央的一支手機 | 871 × 669 | 951 × 669 |
| 展開並旋轉 | 375 × 667 | 669 × 871 | 669 × 951 |
| 闔上並旋轉 | 667 × 375 | 678 × 386 | 678 × 466 |
| 展開時的寬度 size class | compact | regular | regular |
| 各列 | 一律橫向 | 一律橫向 | 垂直,展開並旋轉時除外 |
toolbarVerticalEdge |
nil | nil | trailing,展開並旋轉時為 nil |
| 保留區域 | 無 | 無 | 摺疊處、兩顆相機、狀態欄 |
| 半摺 | 無變化 | 無變化 | 摺疊處成為作用中的分隔區域;分割式 arrangement 在其上留出間隙 |
視窗尺寸以點為單位,取自各建置自身視窗的回報。12
中間那一欄請讀兩遍,因為大多數 App 即將以這種狀態上架。由 Xcode 27 建置的版本,在內側螢幕上拿到 regular 寬度與大部分的螢幕,比起第一欄那種裝在方框裡的手機,確實是很大的進步。但它在後緣前 80 點就停住,那裡是狀態直欄;它拿不到垂直列;系統也完全不告訴它摺疊處或相機的資訊:每一次保留區域的查詢,在每一種姿態下都回傳空的結果,不管探測 App 問了幾次。12 為了確定這個標記是公平的替身,我也用 Xcode 27.0 本身、針對它自己的 iOS 27.0 SDK,編譯了一個陽春版的探測 App。它拿到的視窗一模一樣:闔上 386 × 678,展開 871 × 669。12
Apple 的書面指南在這點上不如演講精確,而且措辭變過。9 月 17 日,它的概觀寫的是「Build your app with Xcode 27.1 or later to use all of the available screen space on iPhone Duo. In earlier versions, your app doesn’t extend under the status bar and camera.」(以 Xcode 27.1 或更新版本建置,才能使用 iPhone Duo 上全部的可用螢幕空間;較早的版本中,App 不會延伸到狀態列與相機底下。)到了 9 月 19 日,它改成了如今 10 月 2 日仍是的樣子:「Build your app with the latest version of Xcode to use all of the available screen space on iPhone Duo. When you build with Xcode 26 and earlier, your app doesn’t extend under the status bar and camera.」(以最新版本的 Xcode 建置,才能使用 iPhone Duo 上全部的可用螢幕空間;以 Xcode 26 或更早版本建置時,App 不會延伸到狀態列與相機底下。)2 新的句子只點名 Xcode 26 及更早的版本做不到,而「latest version」(最新版本)也沒說 beta 算不算,所以讀者可能以為 Xcode 27.0 的建置就能拿到整塊螢幕。在這個 beta 的模擬器裡並非如此。它拿到的是演講中的中間那一級,而先前的措辭才和我量到的結果相符。在實機證明不是這樣之前,請以這張表為準來規劃。
所以,在做任何事之前,先檢查您正在測試的建置帶著什麼標記:
otool -l Build/Products/Debug-iphonesimulator/YourApp.app/YourApp | grep -A4 LC_BUILD_VERSION
Xcode 的 debug 建置會把程式碼放在一個小型啟動器旁邊的 YourApp.debug.dylib 裡,兩者都帶有這個指令。您要找的是 sdk 27.1 或更新的版本;標記為 27.2 的探測 App(也就是 Xcode 27.2 beta 的 SDK 會寫入的值)得到的待遇和 27.1 相同。12 Kiradex 讀出來是 minos 27.0 與 sdk 27.1:它可以安裝在 iOS 27.0 上,並在 Duo 上獲得 27.1 的行為。13
我之所以在這點上著墨這麼多,是因為我曾公開地弄錯過。我在 9 月 21 日發表的這個模擬器的第一篇報告寫道,工具列從未變成垂直,而且每種姿態下的保留區域都是空的。beta 本身沒問題。我當時是直接呼叫 swiftc -sdk 來編譯那支探測 App,連結步驟把同一個 Xcode 裡的 macOS SDK 當成根目錄,產出的二進位檔被標記為 27.0:我那天量到的一切,都是中間那一欄。那篇文章現在已附上更正。12 如果您的 Duo 版面在模擬器裡看起來只採用了一半,各列橫在頂端,側邊還有一條死黑的長條,請先檢查標記,再檢查程式碼。
工作台上的 App
Kiradex 是一款我們放在 TestFlight 上的集換式卡牌收藏 App:收錄每一個系列、您已擁有與想要的卡片、它們隨時間變化的價值,並把每張卡做成可以翻轉的 3D 物件。它免費、僅支援 iPhone、可在 iOS 27.0 及更新版本上執行,收藏者的清單存在他們自己的 iCloud 裡,中間沒有我們的伺服器。13 它還沒完成。掃描器從未碰過真正的相機,而且直到這週,都還沒有人在展開的 Duo 上看過這個 App:在內側螢幕版面旁,它的開發路線圖上寫著這一行:「Seen on the open Duo’s inner display only by the column maths; the simulator was folded shut.」(在展開的 Duo 內側螢幕上只用欄數算過;模擬器一直是闔上的。)13 這讓它成了一個公平的測試對象。它裡面的 Duo 相關工作,全是照著 Apple 的文件寫的,從未對著螢幕檢查過。
它為 Duo 做的事不多,之所以值得列出來,正是因為其中真正屬於 Duo 的程式碼少之又少:
- 導覽交給系統。 一個有五個分頁的
TabView,其中一個是搜尋角色,四個分頁裡各有一個NavigationStack;掃描器是一個沒有自己列的相機畫面。工具列項目是同時具備標題與符號的Label,以.toolbar掛上。整個 App 沒有任何自製的列。 - 版面依 size class 而定。 compact 寬度拿到三欄的卡冊頁面,並把卡片的檢視面板推入導覽堆疊。regular 寬度(對僅支援 iPhone 的 App 而言,大多就是 Duo 的內側螢幕)拿到一整面卡牆,欄數為偶數;點選一張卡後,螢幕就在頁面與檢視面板之間一分為二。
- 兩個來自 27.1 SDK 的呼叫。 分割畫面是
.split樣式的ArrangementView,在 iOS 27.1 以下以HStack作為後備。onHingeChange則在手機從闔上變成其他狀態時,重播 App 的開場動畫:一台紅色裝置翻開。 - 不鎖定方向,並有啟動畫面。
Info.plist列出直向與兩種橫向,並宣告了UILaunchScreen。13
整份清單就這些:兩個檔案裡的兩處可用性檢查。手機對這個 App 所做的其他一切,對任何以這種方式建置的 App 都一樣會做。
本文中的圖片顯示的是 App 實際執行的樣子,卡片圖片來自它所讀取的公開目錄。Kiradex 是一款獨立的收藏者工具,與 Nintendo、Creatures Inc.、GAME FREAK inc. 或 The Pokémon Company 沒有任何關聯,也未獲其背書,卡片圖片的權利歸各自的所有者。
第二步:走過每一種姿態
Apple 的指南把這趟走查列為四項檢查:2
- 「Confirm your views resize well in each supported orientation and pose.」(確認您的檢視在每個支援的方向與姿態下都能妥善調整大小。)
- 「Inspect how the system presents your app’s navigation bars, toolbars, and tab bars vertically on the side of the display.」(檢查系統如何把 App 的導覽列、工具列與標籤列垂直呈現在螢幕側邊。)
- 「Identify any views, sheets, or popovers that position awkwardly when you fold or open iPhone Duo.」(找出在摺起或展開 iPhone Duo 時位置變得彆扭的檢視、sheet 或 popover。)
- 「Identify elements or controls in your views that appear in the fold, and are difficult to see or interact with.」(找出檢視中落在摺疊處、因而難以看清或操作的元素與控制項。)
它的設計指引則說明這需要幾套版面:「A compact width layout for the outer display and a regular width layout for the inner display give you the fundamentals for every pose.」(外側螢幕一套 compact 寬度版面、內側螢幕一套 regular 寬度版面,就涵蓋了每種姿態的基本功。)7
姿態是 Device Hub 裡的按鈕:Closed、Book、Open 與 Rotate Right,每一種組合都是一塊不同的螢幕。SwiftLee 的指南還補上了一個我用不到的更細的控制:「Holding Option ⌥ reveals a slider that you can use to control the hinge angle of the device with precision」(按住 Option ⌥ 會出現一個滑桿,可精確控制裝置的鉸鏈角度)。17 在看 App 之前,先了解系統在每種姿態下交給任何 App 什麼,會很有幫助。以下是探測 App 在 27.1 標記下的讀數:12
| 姿態 | 視窗(點) | Size class(寬、高) | 各列 | 回報的保留區域 |
|---|---|---|---|---|
| 闔上 | 466 × 678 | compact、regular | 垂直,位於後緣 | 外側相機,37 × 37,作用中;狀態欄,84 × 170,作用中 |
| 闔上並旋轉 | 678 × 466 | compact、compact | 垂直,位於後緣 | 外側相機,作用中;狀態區,84 × 82,作用中 |
| 展開 | 951 × 669 | regular、regular | 垂直,位於後緣 | 摺疊處,寬 40,x 455 至 495,非作用中;內側相機,58 × 37,非作用中;狀態欄,84 × 120,作用中 |
| Book | 951 × 669 | regular、regular | 垂直,位於後緣 | 同樣三個,摺疊處為作用中 |
| 展開並旋轉 | 669 × 951 | regular、regular | 橫向 | 摺疊處,高 40,y 455 至 495,非作用中;內側相機,非作用中;狀態區,134 × 82,作用中 |
| Book 並旋轉 | 669 × 951 | regular、regular | 橫向 | 同樣三個,摺疊處為作用中 |
那張表裡有三件事,決定了之後的一切。
手機摺起時,視窗不會變。 Book 與 Open 回報的尺寸相同。App 唯一看得到的差異,是摺疊處的分隔區域從非作用中變成作用中,而鉸鏈從完全展開的 180 度,變成回報半開的 127 度。只讀取自身尺寸的 App,永遠不會知道手機被摺過。
摺疊處永遠存在,而且很窄。 不論手機展開還是摺起,它都會被回報,位置正好在中央,API 回傳的框在兩種狀態下都是 40 點寬,兩側各有 20 點邊距。Apple 的演講談到摺疊處的區域時說:「When flat, it’s inactive and has a width of zero.」(攤平時,它是非作用中的,寬度為零。)(7:32) 模擬器並沒有回傳寬度為零的框。Artem Novichkov 的實測筆記描述了同樣的讀數:「40 pt wide, with 20 pt margins on each side of a zero-width fold line,」(40 點寬,在一條寬度為零的摺線兩側各有 20 點邊距)17 我也這樣理解演講中的零,指的是兩段邊距之間的那條線;這是我的解讀,不是 Apple 文字裡寫的。演講也點出了實務上的重點:「By default, only active ones will be returned, but you can query for inactive ones」(預設只回傳作用中的區域,但您可以查詢非作用中的),以及「in grid-like layouts, you could prefer even numbers of columns when a division region is present regardless of its active state.」(在格狀版面中,只要有分隔區域存在,無論是否作用中,都可以優先採用偶數欄。)(7:16, 7:41)5
保留區域來得晚。 每次啟動時,探測 App 的前幾次計算完全讀不到區域,之後的每一次都讀到完整的一組。只在第一次版面配置時詢問一次的檢視,會快取到一個空陣列。請在檢視的 body 裡、每一次計算時讀取;探測 App 以一秒一次的計時器重新計算,而 Artem Novichkov 的筆記把規則講得很直接:「Read them in the GeometryReader body so the view updates when they arrive. Don’t cache them.」(在 GeometryReader 的 body 裡讀取,這樣區域抵達時檢視就會更新。不要快取它們。)1217
看 App 之前,還有一則實務提醒。這個 beta 的 iOS 27.1 模擬器執行環境只支援一種裝置類型,就是 iPhone Duo;要它建立 iPhone 18 Pro Max 會以「Incompatible device」失敗。因此下文的比較,也就是同一個建置在一般 iPhone 上的表現,是在 iOS 27.0 執行環境上跑的,這也正好是確認 #available 後備路徑能否運作的快速方法。12

同一個 Kiradex 建置,分別在 6.9 吋 iPhone、闔上的 Duo 與展開的 Duo 上。App 的程式碼裡沒有任何東西把各列放到側邊。
走查發現了什麼
一個 UI 測試讓 build 5 在六種姿態下各走過八種畫面(在闔上並旋轉時是七種,因為「設定」按鈕移進了溢位選單),同時由一支指令稿在每一站擷取兩塊螢幕:外側 1398 × 2034 像素,內側 2853 × 2007,也就是 App Store Connect 列出的尺寸。13 對照 Apple 的四項檢查來看,這個 App 通過的項目比我預期的多,失敗的地方則是任何程式碼閱讀都沒有標出來的。
不必動手就到位的
各列。 闔上時,工具列與全部五個分頁都立在時鐘下方的直欄裡;展開時也一樣;展開並旋轉時,它們回到頂端與底部。App 完全沒有要求這些。第二場演講說明了自製的列為什麼會被拋在後頭:「When used to build custom bars, content from sub-components like UIToolbar, UINavigationBar, or UITabBar won’t be considered.」(用來建構自訂列時,UIToolbar、UINavigationBar 或 UITabBar 等子元件的內容將不會被納入考量。)(2:52)4 而且由於主要畫面上的每個工具列項目都是 Label,每一個都有給直欄用的符號,也有給溢位選單用的標題,這正是指南的另一個條件:「If your item has a title and doesn’t have an icon, the system doesn’t present it vertically.」(如果項目只有標題而沒有圖示,系統不會以垂直方式呈現它。)2
摺疊處。 展開時,一個系列的卡片排成六欄,是偶數,所以沒有任何一張卡落在螢幕正中央。點選一張,螢幕就在 App 內容區的中點 x 433 處,分成頁面與卡片的檢視面板。那比摺疊處偏左 42 點,因為直欄從右側拿走了 84 點;手機攤平時,這無關緊要。在 Book 姿態下,arrangement view 把分隔線移到摺疊處,並讓那條 40 點的帶子空著:原本從 x 433 開始的檢視面板,現在從 495 開始。App 完全沒有針對 Book 姿態的程式碼;這是 ArrangementView 在做演講所描述的事:「moving, resizing, or reorganizing what’s already there.」(移動、調整大小或重新組織已有的內容。)(6:04)513

先是展開攤平,再是 Book 姿態。第二張圖裡的間隙就是摺疊處,不是 App 放上去的。
展開時的 sheet。 在內側螢幕上,sheet 置中出現,它的 Done 按鈕維持橫向;在 Book 姿態下,探測 App 的 sheet 自行移到了摺疊處靠前導邊(leading)的那一側。12
鉸鏈,作為一個事件。 App 執行中展開手機,它的開場動畫會在內側螢幕上再播一次:紅色裝置先是闔著,接著翻開,進入 App。這只是一個 onHingeChange 處理常式,在狀態離開 closed 時觸發,而這正是第四場演講要求的分工:「Hinge data is observed live, and is ideal for driving interactions or effects. For layout, use the arrangement and region APIs.」(鉸鏈資料是即時觀察的,非常適合驅動互動或效果;版面配置則請使用 arrangement 與 region API。)(2:34)6

App 執行中展開手機:開場動畫是 App 自己的裝置,由 App 繪製,由鉸鏈觸發重播。
沒能顧到的
只有一個文字按鈕的 sheet,保留了直欄卻讓它空著。 闔上時,「設定」sheet 沿著後緣讓出一條給垂直列用的帶子,然後什麼也沒放進去:唯一的工具列項目是一個只有標題、沒有符號的「Done」,而純文字項目會維持橫向。表單被擠進剩下的空間。探測 App 量出了代價:保留直欄時內容寬 374 點,關閉垂直列時是 450 點。12 Apple 的演講點名了這種情況:「if a sheet is control-heavy with only one item, like the close button here, consider disabling a vertical bar so it doesn’t reduce available space.」(如果 sheet 以控制項為主、列上卻只有一個項目,例如這裡的關閉按鈕,不妨停用垂直列,以免縮減可用空間。)(14:37)4 修法有兩種,探測 App 兩種都試過。給按鈕一個符號,Button("Done", systemImage: "checkmark"),它就會以醒目的勾號移進直欄;或者在 sheet 的內容上加上 .toolbarVerticalBehavior(.disabled),直欄就會消失,代價是 sheet 變矮。

由左至右:App 的 sheet,接著是探測 App 的純文字 Done、加上符號的 Done,以及停用垂直列的版本。
全螢幕卡片跑到相機底下。 這個 App 的招牌畫面,是一張卡片放在黑色舞台上、各列隱藏,而它的舞台在每一側都忽略安全區域。在外側螢幕上,這讓卡片的上緣一角跑到相機底下。系統會把那顆相機回報給任何詢問的檢視,是一個 37 點見方、作用中的遮蔽區域,其位置與 Apple 自家外框圖中的開孔誤差在一點半以內。1112 Apple 的設計指引允許全寬處理,「as long as nothing conflicts with the Dynamic Island or the status bar.」(只要沒有任何東西與動態島或狀態列衝突。)7 修法是讓黑色底保持滿版出血,但把卡片本身收進來:要嘛讓舞台在這塊螢幕上遵守後緣與上緣的安全區域,要嘛讀取 reservedRegions(kind: .occlusion),讓卡片的框避開回傳的區域。

檢視器的擷取圖,放在 Apple 外側螢幕的外框裡。模擬器的截圖上沒有洞,手機上有。
旋轉後,兩個窗格上下堆疊。 展開並轉成直向時,螢幕寬 669 點,仍是 regular 寬度,所以 App 仍會分割。但填滿一個高大於寬之空間的 arrangement view,會上下分割,Apple 的指南是這麼寫的:「it places the primary view on top and the secondary view below it when the containing view is taller than it is wide.」(當容器檢視高大於寬時,它會把主要檢視放在上方、次要檢視放在下方。)2 結果是一條只顯示一列卡片加上第二列頂端的長條,下面是檢視面板。App 在那一行上自己的註解說,這一對「is only useful side by side,」(只有並排時才有用),卻沒有任何東西強制執行這一點。用 .split.axes(.horizontal) 限制樣式是文件記載的控制方式,16 但有個陷阱,演講講得很清楚:「If the split arrangement cannot split among an axis, and it’s the primary axis, the arrangement view chooses to only show a single view.」(如果分割式 arrangement 無法沿某個軸分割,而那正是主軸,arrangement view 會選擇只顯示單一檢視。)(12:44)5 探測 App 證實了兩半:填滿旋轉後的螢幕時,一般的 split 上下堆疊,而只允許水平分割的 split 只顯示主要窗格,別無其他。12 對這個 App 來說,那會把檢視面板藏起來,所以誠實的修法是一個決定,而不是一個修飾符:當空間高大於寬時,像 compact 版面那樣把檢視面板推入導覽堆疊。

旋轉後:各列為橫向,正如 Apple 的指引所說;而分割的方向,對這一對檢視來說是錯的。
跨越展開而保留下來的卡片,兩套版面都沒落進去。 闔上時,點選一張卡片會把它的檢視面板推入導覽堆疊。在顯示那個檢視面板的情況下展開,卡片還在,這是最容易出錯的部分。但它仍是一個被推入的畫面,如今寬 951 點:左邊是舞台,詳細資訊在一個漂在黑底上的白框裡,直欄裡有一個返回按鈕。App 為這塊螢幕設計的是卡牆加上旁邊的卡片,而要到達那裡,唯一的方法是返回再點一次卡片。同一個選取狀態驅動兩套版面;同一條導覽路徑卻沒有。修法是察覺在選取了卡片的情況下從 compact 寬度變成 regular 寬度,然後把被推入的畫面彈出,讓分割畫面接手。

展開前後的同一張卡片。狀態保留下來了;版面卻是 compact 那一套,被拉寬了。
regular 寬度並不等於「Duo」。 這個 App 把 regular 寬度當成內側螢幕:卡牆、偶數欄、分割畫面。橫放的 6.9 吋 iPhone 同樣是 regular 寬度,高度則是 compact,而這個 App 並未鎖定方向。在那裡執行時,同一個分支會產生一個從左緣內縮的頁面、一個詳細資訊欄窄到放不下卡名的檢視面板,以及在螢幕底緣被截斷的動作按鈕。這些都不是 iOS 27 才有的,而且在我跑過的任何 Duo 姿態裡都沒有出現;是 Duo 的工作讓它浮上檯面,因為這是第一次有人在每一塊回報 regular 寬度的螢幕上走過這套版面。區分兩者的檢查是另一個 size class:內側螢幕在兩個維度上都是 regular,而 Apple 的演講說,外側螢幕橫放時在兩個維度上都是 compact。3 需要兩個方向都有空間的版面,就應該兩個都問。

regular 寬度版面在一支不是 Duo 的手機上:橫放的 6.9 吋 iPhone,iOS 27.0。
鍵盤蓋住了直欄。 外側螢幕上鍵盤升起時,直欄底部的分頁會被擋在後面。這是系統的版面配置,不是缺陷;演講說,列可能需要溢位,「as other competing UI appears, like the keyboard」(當其他競爭空間的介面出現時,例如鍵盤)(11:50)。4 它之所以在這份清單上,是因為它弄壞了工具:我第一次走查時,在搜尋鍵盤升起的情況下點了「Collection」,結果點到的是字母 P。假設標籤列永遠點得到的 UI 測試,會最先在這裡失敗。
還有兩件小事。這個 App 依寬度 size class 選擇偶數欄,而演講的建議是依摺疊處本身的區域,無論它是否作用中。另一件與摺疊毫無關係:在 6.9 吋模擬器上連續開啟四次全螢幕檢視器,第一次顯示了卡片,接下來三次是一片空白的黑畫面。這兩件都不會擋住 TestFlight 建置。在 App 跑上螢幕、並有東西依序按下它的按鈕之前,這兩件都看不見。
模擬器看不到的
掃描器。它會打開後置廣角相機,並且已經使用旋轉協調器,這正是相機演講所要求的:「On iPhone Duo, the rotation coordinator will update when your app moves displays.」(在 iPhone Duo 上,當 App 換到另一塊螢幕時,旋轉協調器會隨之更新。)(8:19)6 相機工作階段能否在從一塊螢幕移到另一塊時存活下來,是實機才能回答的問題;模擬器根本沒有相機。Apple 的發行說明又把 StandBy 與大多數 App Extension 列為這個執行環境無法執行的項目,而 Xcode 27.2 beta 2 的說明則為自動擷取的人補上一項:「Screenshots and recordings in iPhone Duo may be black for up to a few minutes after booting the device.」(裝置開機後最多幾分鐘內,iPhone Duo 中的截圖與錄影可能是全黑的。)20
一份原始碼,兩個 Xcode
時程造成了一個問題。商店接受的建置來自 Xcode 27 與 27.0 SDK;在 iPhone Duo 上表現正確的建置來自 27.1 SDK。在 Apple 讓商店接受後者之前,一個既想推出更新、又想繼續做 Duo 版面的 App,必須能在兩者之下編譯。
if #available(iOS 27.1, *) 做不到這一點。可用性是執行期的問題;編譯器仍然必須找得到那個符號。Kiradex 正是用這種方式包住它的兩個 Duo 呼叫,部署目標是 27.0,所以這個建置可以裝在尚未更新的手機上。在 27.1 SDK 下,它能編譯。在 Xcode 27.0 下,做同樣事情的檔案會停在 error: cannot find 'ArrangementView' in scope,因為 27.0 SDK 裡沒有這個型別。14 Kiradex 還沒上架,所以它大可一直留在 beta 工具鏈上;有客戶的 App 就不行。
Swift 文件記載的條件也分不出這兩個 SDK。#if compiler(>=6.4) 在兩者下都成立:Xcode 27.0、Xcode 27.1 beta 與 Xcode 27.2 beta 都印出同樣的 Apple Swift version 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1)。14 剩下兩道圍欄,我兩道都試過。
自行設定的編譯條件。 這是有文件記載的做法。只在用 beta 建置的組態裡定義一個旗標(在 Xcode 中是 SWIFT_ACTIVE_COMPILATION_CONDITIONS = DUO_SDK;在命令列上是 -D DUO_SDK),再用它把只屬於 27.1 的程式碼圍起來:
struct Pair<Primary: View, Secondary: View>: View {
@ViewBuilder var primary: Primary
@ViewBuilder var secondary: Secondary
var body: some View {
#if DUO_SDK
if #available(iOS 27.1, *) {
ArrangementView { primary } secondary: { secondary }
.arrangementViewStyle(.split)
} else {
HStack(spacing: 0) { primary; secondary }
}
#else
HStack(spacing: 0) { primary; secondary }
#endif
}
}
不加旗標時,這個檔案在 Xcode 27.0 與 27.1 beta 下都能通過型別檢查。加上旗標時,它在 beta 下通過,在 27.0 下則以同樣的找不到符號錯誤失敗:錯誤的搭配建置不起來。14
由編譯器讀取 SDK 自身的版本。 canImport 接受一個以底線開頭的第二個引數,用來和模組版本比較,而 SwiftUI 的模組版本在各 SDK 之間不同:27.0 SDK 是 8.0.84.1.104,27.1 SDK 是 8.0.85.27,27.2 beta SDK 是 8.1.6.1.101。14 所以這種寫法不需要任何建置設定:
#if canImport(SwiftUI, _version: 8.0.85)
// ArrangementView, reservedRegions, onHingeChange, toolbarVerticalBehavior
#else
// what the app did before
#endif
我在每個分支裡放了一個 #warning,用三個 Xcode 各編譯一次:27.0 走第二個分支,27.1 beta 與 27.2 beta 走第一個。14 要小心的地方就在那個底線。Swift 官方書籍中這個條件的語法是 canImport(import-path),僅此而已,所以 _version: 是一項沒有文件保證的編譯器功能,而 8.0.85 這個數字是我從兩份 SDK 裡讀出來的,不是 Apple 公布的。14 若您的環境不方便加建置設定,可以在商店仍不接受 27.1 的期間使用它;若您想要一個站得住腳的做法,就用旗標。
不論哪一種,圍欄都是暫時的。等到 App Store Connect 接受帶有 27.1 SDK 之 Xcode 的建置那天,就把它刪掉,保留 #available 檢查,因為保護仍停在 iOS 27.0 之手機的,正是這些檢查。
送審:App Store Connect 現在接受什麼
TestFlight 接受 27.1 建置。 App Store Connect 9 月 18 日的發行說明:「You can now submit apps built with Xcode 27.1 beta using the SDK for iOS 27.1 beta or iPadOS 27.1 beta for internal and external testing.」(現在可以提交以 Xcode 27.1 beta、使用 iOS 27.1 beta 或 iPadOS 27.1 beta SDK 建置的 App,進行內部與外部測試。)9 月 16 日與 9 月 28 日的條目,對 Xcode 27.2 beta 與 beta 2 說了同樣的話。8 Kiradex 就是這樣上傳的;它的第五個建置,也就是本文測試的那一個,於 10 月 2 日上傳,App Store Connect 將其列為有效。13
App Store 還不接受。 最新一則向商店開放的條目是 9 月 14 日:「You can now upload apps built with Xcode 27 using the SDK for iOS 27.0, iPadOS 27.0, macOS 27.0, tvOS 27.0, visionOS 27.0, and watchOS 27.0 for the App Store, and for internal and external testing through TestFlight.」(現在可以上傳以 Xcode 27、使用各平台 27.0 SDK 建置的 App,供 App Store 上架,以及透過 TestFlight 進行內部與外部測試。)8 在那之後,沒有任何條目在同一句話裡同時提到 27.1 與商店;而 Apple 的發佈摘要(最新項目日期為 9 月 28 日)只列出一個 Xcode 27.1 建置,也就是 9 月 18 日的 beta。8 Apple 沒有說這何時會改變。裝置則在 10 月 23 日開賣。1
除非 Apple 在 10 月 23 日之前讓商店接受 27.1 SDK,否則您的客戶在上市當天拿到的商店版建置,充其量是 27.0 SDK 的建置;上面的比較說明了它在模擬器裡拿到什麼,而 Apple 的演講描述的也是同樣的中間那一級:狀態直欄旁的螢幕、橫向的列、沒有摺疊處資訊。只要它能妥善調整大小,那就是一個堪用的 App,而這正是現在就用 Xcode 27 推出調整大小相關工作的理由。
啟動畫面現在是強制的。 這不是 Duo 的規則,但它隨同一個 SDK 而來,而且會在上傳時失敗,不是在審查時。Apple 的技術說明:「Starting in iOS 27 and iPadOS 27, App Store Connect requires your app to include a launch screen configuration in its Info.plist,」(自 iOS 27 與 iPadOS 27 起,App Store Connect 要求 App 在其 Info.plist 中包含啟動畫面設定),若缺少 UILaunchStoryboardName、UILaunchStoryboards、UILaunchScreen 或 UILaunchScreens 其中之一,上傳會以「ITMS-90870: Missing launch screen.」被拒。10 Kiradex 宣告了帶有顏色的 UILaunchScreen,用的是封面的紅色,所以啟動畫面會直接銜接開場動畫。13
截圖欄位只存在於紙上。 App Store Connect 的截圖規格自 9 月 9 日起就列出了 iPhone Duo,分成兩組:外側螢幕為 1398 × 2034 或 2034 × 1398 像素,內側螢幕為 2007 × 2853 或 2853 × 2007,並附註:「Support for uploading assets for this device in App Store Connect will be available later this year.」(App Store Connect 對此裝置上傳素材的支援將於今年稍晚推出。)9 一般的規則照樣適用:「You can upload one to 10 screenshots in .jpeg, .jpg, and .png formats,」(您可以上傳 1 到 10 張 .jpeg、.jpg 與 .png 格式的截圖)以及「Images can’t include alpha channels or transparencies.」(圖片不能包含 alpha 色版或透明度。)9
上傳頁面上有一句話,決定了您該怎麼圍繞這件事來規劃:「Once your app is submitted for review and approved, you must create a new version to update the screenshots.」(App 一旦提交審查並獲核准,就必須建立新版本才能更新截圖。)9 Duo 截圖無法事後單獨補上;欄位開放時,它們得搭著一個版本一起上。
綜合起來,我會採用、而且正在為 Kiradex 採用的順序是:
- 現在就把 27.1 建置放上 TestFlight,走查的發現隨出現隨修。
- 對於已上架的 App:用 Xcode 27 建置一個更新,帶上所有不需要 27.1 SDK 的修正。以 size class 取代 idiom 與方向判斷、分邊處理的安全區域、由系統容器擁有的列,以及每個工具列項目都有標題與符號。
- 現在就從模擬器以 Duo 尺寸擷取截圖,並保存下來。
- 準備好一個版本,等兩件事同時成立的那一天:帶有 27.1 SDK 的 Xcode 獲准用於上架,而且 Duo 欄位開放上傳。到目前為止,Apple 每一次這類變更都公布在 App Store Connect 的發行說明裡,包括 9 月 14 日向 Xcode 27 正式版開放商店的條目,以及 9 月 9 日加入 Duo 截圖規格的條目,所以那就是要盯的頁面,旁邊再加上開發者新聞頁面。8
還有一項要求在更遠的地方。自 2027 年 4 月起,「iOS and iPadOS apps must be built with the iOS 27 & iPadOS 27 SDK or later」(iOS 與 iPadOS App 必須以 iOS 27 與 iPadOS 27 SDK 或更新版本建置),否則根本無法上傳。18 仍以 iOS 26 SDK 建置的 App,在 Duo 上只能拿到上述三種畫面中最小的那一種,而且從 2027 年 4 月起,在改用 iOS 27 SDK 之前,它無法上傳任何更新。
兩塊螢幕的截圖
走查已經產出了素材:每一站都以 1398 × 2034 與 2853 × 2007 像素擷取,旋轉後則是 2034 × 1398 與 2007 × 2853,正好是 App Store Connect 的 iPhone Duo 那一列所列的四種尺寸。913 剩下的是決定在它們周圍放什麼,而摺疊手機恰恰在這裡誘人犯錯。
大家都想要的畫面,是手機以某個角度半開、App 的內容從摺疊處傾瀉而出。Apple 的行銷指南逐條排除了它。對於 Apple 自己的裝置圖片:「Use Apple product images ‘as is’ and without modification. Modifications include adding reflections, shadows, highlights, or graphic elements that appear to enter or come out of the product screen; cropping, tilting, or obstructing any part of the images; animating, flipping, or spinning the images」(Apple 產品圖片須「原樣」使用、不得修改,修改包括加上反光、陰影、高光或看似進出產品螢幕的圖形元素,裁切、傾斜或遮擋圖片任何部分,以及讓圖片動起來、翻轉或旋轉)。對於您自己製作的影像:「Straight-on product shots are preferred. Don’t use extreme angles or alter an Apple product in any way.」(以正面產品照為佳;不要使用極端角度,也不要以任何方式改動 Apple 產品。)而在「Unauthorized Uses」(未經授權的用途)之下,清單第一項是:「Rendering in 3D or creating any simulation of an Apple product」(以 3D 算圖或製作任何 Apple 產品的模擬)。11 我的截圖文章談過得獎作品如何對待這些規則,以及遵守它們的一組截圖是什麼樣子;多了一個鉸鏈,並不會改變這些規則。
Apple 改為提供的,是這支手機的一套外框素材,兩種外觀、五種視角:闔上的手機的直向與橫向、展開的內側螢幕的橫向與直向,以及從背面看展開的手機,外側螢幕在相機旁邊。這些檔案中的開口正好是截圖尺寸,所以模擬器的擷取圖不必縮放就能放進去。11 沒有半摺的視角,也沒有傾斜的視角。
所以下面的畫面由三個部分組成:使用 App 自身配色的底色、一行簡短的文字,以及放在 Apple 外框裡、完整而直立、上面什麼都不疊的擷取圖。變化來自底色、裝置的大小,以及每組中一張完全沒有裝置的畫面:App 的全螢幕卡片放在黑底上,那是 App 自己的 3D,不是任何人的硬體。

三組由指令稿以走查擷取圖合成的截圖,尺寸為 1320 × 2868、1398 × 2034 與 2853 × 2007 像素。它們展示的是方法,不是實際的商店頁面:這些擷取圖帶著目錄裡的卡片,而商店那一組要放什麼,正是下面第三則提醒裡懸而未決的問題。
製作它們時得到的三則實務提醒。
用指令稿合成。 上面那幾組出自一個 Python 檔,它接收一張擷取圖、一句標題與一種底色,輸出一張尺寸精確、沒有 alpha 色版的 PNG。當 App 變動時(這一個在我擷取當天就變了兩次),走查重跑一遍,畫面就重建一遍。
擷取手機多給的東西。 Apple 的上傳頁面說,若介面在每種尺寸上都相同,一組 6.9 吋截圖就夠了:「provide only the highest resolution screenshots required. They automatically scale down to smaller device sizes.」(只需提供所需的最高解析度截圖,它們會自動縮小到較小的裝置尺寸。)9 在這支手機上並不相同:卡牆與分割的檢視面板只存在於內側螢幕。內側那組就以這兩張畫面開場。
留意螢幕上有什麼。 同一份指南說:「You are responsible for securing the rights to all materials used in screen content within your app.」(您有責任取得 App 螢幕內容中所用一切素材的權利。)11 收藏 App 本質上就會顯示別人的美術作品,而商店頁面是行銷,和 App 在使用中顯示什麼是兩回事。我們還沒為 Kiradex 做出這個決定,上面的畫面也不會原樣送出。正因如此,走查有第二種模式,使用六張虛構的卡片執行;它們目前還沒有美術,所以要送出一組截圖,就得設計替代圖,或者把權利問題處理好。
Kiradex 還沒有商店頁面。等到有了,它會從一組 6.9 吋截圖開始,而 Duo 的截圖組則搭著自己的一個版本,等待 App Store Connect 接受它們的那一天。
把工作交給程式碼代理
這份工作大部分是代理擅長的那種,前提是它看得到 App。分成兩半,其中一半 Apple 已經提供了。
靜態的那一半屬於 Apple。 第一場 Tech Talk 就以它收尾:「During the talk, Modernize Your UIKit App, we introduced a new app modernization skill. With Xcode 27.1, this skill has a new name: App Resizability. It now supports SwiftUI and iPhone Duo.」(我們在「Modernize Your UIKit App」演講中介紹了一項新的 App 現代化技能。在 Xcode 27.1 中,這項技能有了新名字:App Resizability,現在也支援 SwiftUI 與 iPhone Duo。)(9:19)3 這項技能是 beta 裡的一組純文字檔案:在 Xcode 27.1 beta 中是 Contents/PlugIns/IDEIntelligenceChat.framework/Versions/A/Resources/app-resizability.idechatprompttemplate,旁邊還有五個參考檔。15 它的指示說明了範圍要撒多廣:「Treat a request about the foldable iPhone Duo as a request for every task in the Task Registry, because a screen that changes size exposes all of them at once.」(把關於可摺疊 iPhone Duo 的請求,視為對 Task Registry 中每一項任務的請求,因為會改變大小的螢幕會一次暴露所有問題。)15
即使您從不執行它,它也值得一讀,因為那是 Apple 寫下來的審查清單。它檢查三項專案設定、一項也不修改(啟動畫面設定鍵、iPad 方向宣告、UIRequiresFullScreen),接著搜尋五種模式:UIScreen.main、依介面方向決定的版面、依 userInterfaceIdiom 決定的版面、該用 scene 生命週期卻用了 application 生命週期,以及假設左右兩側相同的安全區域程式碼。安全區域那個檔案裡有一句話,解釋了這支手機上一半的問題來源:「Asymmetric horizontal insets are the normal case. Where a vertical bar is present, one horizontal edge usually carries the whole inset and the other carries zero.」(左右不對稱的邊距是常態。有垂直列時,通常一側承擔全部的邊距,另一側為零。)15
Kiradex 是今年寫的 SwiftUI App,那五種搜尋在它身上一無所獲。13 上面的每一項發現都來自實際執行。這就是靜態那一半的極限:它讀的是原始碼,而摺疊處、直欄與鍵盤,只在螢幕上看得見。
另一半,是一趟代理能自己跑、自己看的走查。 讓上面的稽核成為可能的,是三個小零件,沒有一個專屬於這個 App:
- 一個造訪每一種畫面、並在每一處停下的 UI 測試。 不是斷言,是一趟巡覽。我們的會開啟 Dex、一個系列、一張卡片、一個 sheet、收藏清單、收藏中的一張卡片、全螢幕檢視器,以及鍵盤升起的搜尋:共八站。13
- 在 Mac 上進行、而不是在測試裡進行的擷取。 UI 測試自己的截圖只涵蓋一塊螢幕。
simctl兩塊都拿得到:
xcrun simctl io "$UDID" screenshot --type=png --display=primary outer.png
xcrun simctl io "$UDID" screenshot --type=png --display=primary-1 inner.png
測試寫出一個以該站命名的空檔案;Mac 上的 shell 迴圈看到它,擷取兩塊螢幕,再寫出第二個檔案作為回應;測試等到那個檔案出現就繼續往下走。外側擷取圖在手機闔上時是 1398 × 2034 像素,內側在展開時是 2853 × 2007,所以稽核用的圖片與商店的截圖出自同一次執行。913
3. 一種不必用手碰滑鼠就能切換姿態的方法。 simctl 沒有姿態指令,而這個 beta 的 UI 測試框架裡,我也找不到任何鉸鏈相關的呼叫,所以指令稿透過輔助使用 API 按下 Device Hub 的 Closed、Book、Open 與 Rotate Right 按鈕,Device Hub 在背景時也能運作。13
有了這些,檢查 App 在每種姿態下的表現,就不再是徵詢代理的意見,而變成每種姿態一個資料夾的圖片,它和您都能閱讀。下面是我會交出去的工作說明,寫成可以直接貼上的形式。
Goal: make this app correct on iPhone Duo in every pose, without device checks.
0. Toolchain. Build with Xcode 27.1 beta. Confirm the binary is stamped with the 27.1 SDK:
otool -l <App>.app/<App> | grep -A4 LC_BUILD_VERSION (debug builds: <App>.debug.dylib)
If "sdk" is lower than 27.1, stop: nothing below will reproduce.
1. Static pass. Report every use, with file and line, of:
UIScreen.main / UIScreen.mainScreen; userInterfaceIdiom; interface orientation used for layout;
a UIApplicationDelegate doing scene work; one safe-area inset applied to both sides;
a bare ignoresSafeArea() on anything a person reads or taps; fixed widths tied to a phone size;
toolbars or tab bars built by hand instead of owned by NavigationStack, NavigationSplitView or TabView;
toolbar items with a title and no symbol, or a symbol and no title.
Confirm Info.plist has a launch screen key and no orientation lock the design does not need.
2. Walk. Run the screenshot tour in each pose and capture BOTH displays at every stop:
closed; closed and turned; open; open and turned; partly folded; partly folded and turned.
If no tour exists, write a UI test that stops at each kind of screen and signals a host script, and have
the script capture with: xcrun simctl io <udid> screenshot --display=primary (outer)
xcrun simctl io <udid> screenshot --display=primary-1 (inner)
Poses are buttons in Device Hub (Closed, Book, Open, Rotate Right); simctl has no pose command.
3. Read every capture and answer, per pose:
- Are the bars where the system puts them (down the side closed and in open landscape, across the top
and bottom in open portrait)? Is any toolbar item missing from the bar and from the overflow menu?
- Does any sheet reserve the side rail and leave it empty?
- With the keyboard up, what is covered? Can every tab still be reached once it is dismissed?
- Does anything sit under the outer camera corner or the status column?
- Open: does a grid have an even number of columns? Does any control or line of text cross the middle?
- Partly folded: does content move off the fold? Do sheets and alerts land on one side?
- Turned: did a two-pane layout stack when it should have stayed side by side, or the reverse?
- Is the same selection, scroll position and navigation path still there after the pose changed?
4. Fix with, in this order: a system container that already adapts; size classes; ArrangementView for a
custom two-pane layout; reservedRegions for hand-placed content. Never branch on the device model,
the idiom, the orientation, or the raw hinge angle to decide layout.
5. Guard everything from the 27.1 SDK with `if #available(iOS 27.1, *)` and a fallback that keeps both
panes reachable. If the same source must also build with Xcode 27.0, fence it at compile time.
6. Report what could not be checked in the simulator: cameras, StandBy, extensions, haptics, real reach.
Apple 的資料,依我會使用的順序
Apple 為這台裝置發布的所有資料,都掛在同一個頁面底下:Get ready for iPhone Duo。19 依工作順序排列:
| 資料 | 為什麼要打開它 |
|---|---|
| Prepare your app for iPhone Duo(Tech Talk) | 三個 SDK 等級、size class、安全區域。從這裡開始 |
| Preparing your app for iPhone Duo(指南) | 以文字涵蓋同樣的內容,並點名每一個 API。「Address common layout and resizing considerations」一節下的四項檢查,就是稽核清單2 |
| Raise the bar with iPhone Duo | 垂直列:哪些放進去、哪些留在外面、溢位 |
| Strike a pose with adaptive layouts on iPhone Duo | 摺疊處:位移、保留區域、arrangement view |
| Designing for iPhone Duo(HIG)與 Design for iPhone Duo | 設計師會拿來要求您的規則7 |
| Leverage multiple displays and scenes on iPhone Duo | 鉸鏈、Split View、外側螢幕上的第二個 scene |
| Build a great camera experience for iPhone Duo | 只有在您需要拍攝時:兩顆前置相機,以及各自朝向哪一邊6 |
| Group Lab 錄影,第 1 天與第 2 天,以及 SwiftUI、UIKit、Photos and Camera 的論壇問答 | Apple 工程師回答開發者的問題19 |
| Apple Design Resources | Figma 與 Sketch 套件,以及行銷用的產品外框11 |
| Apple 以外:SwiftLee 的模擬器指南、iPhone Duo by Examples、BleepingSwift 的檢查清單、Adapty 的指南 | 一份測試清單與鉸鏈滑桿;每個 API 一個可執行的範例,附實測筆記;一份簡短的檢查清單;我找到唯一一份以付費牆為例走完整個流程的指南17 |
| 工作坊 | 實體參加。SwiftLee 說它們附帶「with the opportunity to test your app on a physical device,」(在實機上測試 App 的機會),在 10 月 23 日之前,這是檢查相機的唯一途徑1719 |
我沒有讀過 Group Lab 的問答整理,也沒讀過論壇討論串:Apple 的論壇頁面對自動化擷取一律回應人機驗證頁面,而我沒有去繞過它。
重點整理
- 如果您現在就有上架的 App: 現在就用 Xcode 27 建置,把調整大小的相關工作推出去。如果 10 月 23 日當天商店仍不接受 27.1 SDK,這就是您的客戶在 Duo 上會拿到的版本,而且其中沒有任何一項需要 beta。
- 如果您要採用 Duo 的 API: 用 Xcode 27.1 beta 建置,以
otool確認sdk 27.1或更新的版本,把建置留在 TestFlight,而如果同一份原始碼仍需為商店建置,就把只屬於 27.1 的呼叫圍起來。 - 如果您負責測試: 不要讀程式碼,去跑姿態。闔上、展開、半摺,各自再旋轉一次,加上一個 sheet、鍵盤、一個全螢幕畫面,以及一個跨越展開而保留下來的畫面。每一次都擷取兩塊螢幕。
- 如果您負責商店頁面: 現在就以 Duo 尺寸擷取,用指令稿合成,以正面方式使用 Apple 的外框,並準備好一個版本,等欄位開放那天。
- 如果您要把這件事交給代理: 給它走查,而不只是檔案。Apple 的 App Resizability 技能涵蓋讀得出來的問題;上面的每一項發現,都是靠看才找到的。
常見問題
我現有的 App 不做任何修改,能在 iPhone Duo 上執行嗎?
可以。Apple 的演講承諾任何 SDK 都可以,模擬器也證實了這一點:標記為 iOS 26 SDK 的建置以 375 × 667 點的視窗執行,標記為 27.0 的則填滿狀態直欄旁的螢幕,各列為橫向。兩者都拿不到垂直列或保留區域。312
要完整的 iPhone Duo 版面,我需要哪個 Xcode?
Xcode 27.1 beta(27A9269),它帶有 iOS 27.1 SDK,以及唯一的 iPhone Duo 模擬器。它的 27.1 模擬器執行環境只支援 iPhone Duo 這一種裝置類型,所以其他 iPhone 要在 27.0 執行環境上測試。Xcode 27.2 beta 的 SDK 有相同的 API,標記為 27.2 的探測 App 在模擬器裡的表現也和 27.1 一樣,但那個 Xcode 沒有 Duo 可以執行。81214
我今天能把 iPhone Duo 建置送上 App Store 嗎?
截至 2026 年 10 月 2 日,以 27.1 SDK 建置的不行。App Store Connect 接受 Xcode 27.1 與 27.2 beta 的建置用於 TestFlight 的內部與外部測試,接受 Xcode 27 的建置用於上架。Apple 沒有公布改變的日期。8
App Store Connect 要的 iPhone Duo 截圖尺寸是多少?
外側螢幕為 1398 × 2034 或 2034 × 1398 像素,內側螢幕為 2007 × 2853 或 2853 × 2007,1 到 10 張,不含 alpha 色版。上傳功能標示為「later this year」(今年稍晚)推出。9
如何擷取 iPhone Duo 模擬器的兩塊螢幕?
外側螢幕用 xcrun simctl io <udid> screenshot --display=primary,內側螢幕用 --display=primary-1。UI 測試自己的截圖只涵蓋一塊螢幕。13
為什麼我的 App 在 iPhone Duo 模擬器裡,各列仍然是橫向的?
有三個原因,依檢查順序排列。二進位檔沒有標記 27.1 SDK。各列是自己做的,而不是由 TabView、NavigationStack 或 NavigationSplitView 擁有。或者內側螢幕處於直向,而 Apple 在設計上就讓各列在那裡維持橫向。2712
我需要為每種姿態各做一套版面嗎?
不需要。Apple 的指引是兩套版面,compact 寬度與 regular 寬度,再加上在內容可能跨越摺疊處時對它做出回應。Kiradex 有這兩套版面與一個 arrangement view;Book 姿態不需要任何程式碼。713
本站相關內容:模擬器文章有安裝步驟、裝置設定檔,以及更正後的各姿態量測;為 iPhone Duo 做設計把 Apple 的設計指引讀成規則;Duo 開發者文章有硬體數字與按日期排列的歷程;截圖文章論證一組截圖應該說什麼;可調整大小的 iPhone 檢查表正是本文要您先推出的 Xcode 27 工作;Xcode 27 彙整文章則追蹤工具鏈。
資料來源
-
Apple Newsroom,Apple unveils iPhone Duo,2026 年 9 月 9 日,於 2026 年 10 月 2 日取得:「Pre-orders begin Friday, October 16, with availability beginning Friday, October 23.」(10 月 16 日週五開始預購,10 月 23 日週五開始供貨。)與「iPhone Duo will be available with iOS 27.1.」(iPhone Duo 將搭載 iOS 27.1 推出。) ↩↩↩
-
Apple,Preparing your app for iPhone Duo,Technology Overviews,2026 年 10 月 2 日透過文件 JSON 端點取得;引用了 Overview 以及「Address common layout and resizing considerations」、「Organize items in your bars」與「Arrange views in different poses」各節。本站於 2026 年 9 月 17 日引用時,Overview 中關於 Xcode 版本的那句話寫法不同(「Build your app with Xcode 27.1 or later to use all of the available screen space on iPhone Duo. In earlier versions, your app doesn’t extend under the status bar and camera.」);本站的 Duo 開發者文章於 9 月 19 日記錄了現行措辭。該頁面沒有任何修訂說明。 ↩↩↩↩↩↩
-
Apple Developer Tech Talk,Prepare your app for iPhone Duo,David Jackson,UI Frameworks。引文取自該演講英文字幕軌的標示時間點。 ↩↩↩↩
-
Apple Developer Tech Talk,Raise the bar with iPhone Duo,Anna(UI Frameworks 工程師)與 Maria(Human Interface Designer)。引文取自該演講英文字幕軌的標示時間點。 ↩↩↩
-
Apple Developer Tech Talk,Strike a pose with adaptive layouts on iPhone Duo,Maria(Human Interface Designer)與 Harry(UI Frameworks 工程師)。引文取自該演講英文字幕軌的標示時間點。 ↩↩↩
-
Apple Developer Tech Talks,Leverage multiple displays and scenes on iPhone Duo 與 Build a great camera experience for iPhone Duo,引文取自英文字幕軌的標示時間點;Apple,Choosing a camera by the direction it faces,AVKit,於 2026 年 10 月 2 日取得。 ↩↩↩
-
Apple,Designing for iPhone Duo,Human Interface Guidelines,2026 年 10 月 2 日透過文件 JSON 端點取得;引用「Device poses」與「Vertical controls」兩節。 ↩↩↩↩↩
-
Apple,App Store Connect release notes,於 2026 年 10 月 2 日取得:引用 2026 年 9 月 14 日與 9 月 18 日的條目;9 月 9 日的條目加入了 iPhone Duo 的截圖規格,並說上傳功能「will be available later this year」,也就是規格頁面重複的那句話;9 月 16 日與 9 月 28 日的條目以相同形式向 Xcode 27.2 beta 與 beta 2 開放 TestFlight,涵蓋六個平台;9 月 18 日之後沒有任何條目提到 Xcode 27.1 或 iOS 27.1 SDK。Apple,Releases,於 2026 年 10 月 2 日取得的 RSS 摘要,最後建置日期為 Mon, 28 Sep 2026 14:00:00 PDT:只有一個項目提到 Xcode 27.1,即日期為 Fri, 18 Sep 2026 的「Xcode 27.1 beta (27A9269)」;最新的 Xcode 項目是日期為 Mon, 28 Sep 2026 的「Xcode 27.2 beta 2 (27B5028f)」。 ↩↩↩↩↩↩↩↩↩↩↩
-
Apple,App Store Connect 說明中的 Screenshot specifications 與 Upload app previews and screenshots,於 2026 年 10 月 2 日取得,引用。 ↩↩↩↩↩↩↩↩↩↩
-
Apple,TN3208: Preparing your app’s launch screen to meet App Store requirements,於 2026 年 10 月 2 日取得,引用;修訂紀錄:「2026-09-14 Updated the ITMS-90870 error message to reflect the iOS 27 launch screen requirement.」與「2026-06-08 First published.」 ↩↩↩
-
Apple,Marketing Resources and Identity Guidelines,「Apple Product Images」、「Unauthorized Uses」、「Screen Content」與「Custom Photography and Video」各節,於 2026 年 10 月 2 日取得,引用。Apple,Apple Design Resources,Product Bezels,iPhone Duo(Photoshop 與 PNG),於 2026 年 10 月 2 日取得。外框的量測由作者進行,依據 Apple
Bezel-iPhone-Duo.dmg中的 PNG 檔:「Outer Closed Portrait」為 1574 × 2194 像素,透明開口為 1398 × 2034;「Inner Open Landscape」為 3093 × 2247,開口為 2853 × 2007;這套素材另有「Inner Open Portrait」、「Outer Closed Landscape」與「Outer Open」,各有 Star White 與 Night Sky 兩種外觀。「Outer Closed Portrait」中的相機開孔,以每點三像素在開口內量測,橫向跨 400.3 至 436.3 點,縱向跨 29.7 至 65.7 點;探測 App 的相機遮蔽區域則橫跨 399 至 436、縱跨 30 至 67。 ↩↩↩↩↩↩ -
作者於 2026 年 10 月 2 日的執行紀錄:macOS 27.0(26A428)、Xcode 27.1 beta(27A9269)、iOS 27.1 模擬器執行環境(24A94401),以及由 iPhone Duo 裝置類型建立的模擬器。探測 App DuoProbe2 是一個 Swift 檔案:一個
TabView,其第一個分頁內有一個帶五個工具列項目的NavigationStack;一個讀出GeometryProxy尺寸、size class、toolbarVerticalEdge、onHingeChange,以及以.includeInactive取得之兩種reservedRegions的顯示區,在 body 的每一次計算時讀取,並每秒重新計算一次;一個 split 樣式的ArrangementView;以及一個記錄其視窗、視窗 scene 的螢幕、UIScreen.main與verticalBarEdgetrait 的 UIKit 檢視。它從該檔案以swiftc針對 27.1 模擬器 SDK 編譯了三次,部署目標 27.1,並告知連結器要記錄的 SDK 版本(-Xlinker -platform_version -Xlinker ios-simulator -Xlinker 27.1 -Xlinker <26.0, 27.0 or 27.1>);對每個二進位檔執行otool -l,印出的sdk值都相符。姿態以 Device Hub 的 Closed、Book、Open 與 Rotate Right 按鈕設定,透過輔助使用 API 按下。主控台輸出,27.1 標記,闔上:size=382x562 ... h=compact v=regular verticalEdge=trailing safe=EdgeInsets(top: 82.0, leading: 0.0, bottom: 34.0, trailing: 84.0) divisions=0 occlusions=2、occlusion[0] active=true frame=(399,-52 37x37)、occlusion[1] active=true frame=(382,-82 84x170)、window=(466.0, 678.0)、verticalBarEdge=2;展開:size=867x553 ... h=regular v=regular verticalEdge=trailing ... divisions=1 occlusions=2、division[0] active=false frame=(455,-82 40x669) margins=EdgeInsets(top: 0.0, leading: 20.0, bottom: 0.0, trailing: 20.0)、occlusion[0] active=false frame=(677,-61 58x37)、occlusion[1] active=true frame=(867,-82 84x120)、window=(951.0, 669.0)、hinge=fullyOpen 180 deg;Book:與展開相同,但為division[0] active=true與hinge=partiallyOpen 127 deg;展開並旋轉:size=669x734 ... verticalEdge=nil ... divisions=1 occlusions=2、division[0] active=false frame=(0,321 669x40)、window=(669.0, 951.0)、mainScreen=(466.0, 678.0)、verticalBarEdge=0;闔上並旋轉:size=594x350 ... h=compact v=compact verticalEdge=trailing、window=(678.0, 466.0)。框的座標以顯示區檢視為準,其原點位於視窗頂端下方 82 或 134 點處。27.0 標記:闔上window=(386.0, 678.0)、展開(871.0, 669.0)、展開並旋轉(669.0, 871.0)、闔上並旋轉(678.0, 386.0),每種姿態的每一行都是verticalEdge=nil與divisions=0 occlusions=0。26.0 標記:闔上、展開、Book 與展開並旋轉時皆為window=(375.0, 667.0),闔上並旋轉時為(667.0, 375.0),全程h=compact。每次以 27.1 標記啟動時,前六到九次計算記錄為divisions=0 occlusions=0,之後區域才出現,其中三次發生在檢視還沒有尺寸之前。展開與 Book 姿態下的窗格位置是從擷取圖量得:攤平時主要窗格從 x 8 到 433.7,次要窗格從 433.7 到 859;Book 時主要窗格到 455.7,一條空帶到 495.7,次要窗格到 859。sheet,闔上:搭配純文字 Done 時內容為374x562,Done 加上符號時相同,使用.toolbarVerticalBehavior(.disabled)時為450x428且verticalEdge=nil;展開:653x501置中,h=compact;Book:459x501,位於前導邊。在 arrangement view 填滿內容區的情況下(一個啟動選項),一般的.split在旋轉後的螢幕上把主要窗格放在次要窗格上方,而.split.axes(.horizontal)只顯示主要窗格;展開攤平時同一個檢視左右分割,在 Book 時則讓摺疊處的帶子空著。依 9 月 21 日那篇文章所述方式編譯的探測 App(swiftc -sdk,且未設定SDKROOT)印出clang: warning: using sysroot for 'macOS 27.0' but targeting 'arm64-apple-ios27.1.0-simulator',並被標記為sdk 27.0。另有兩個建置在闔上與展開時執行過。PlainProbe 有同樣的 tab view、導覽堆疊與工具列,但不含任何來自 27.1 SDK 的東西,由 Xcode 27.0(27A266a)針對其自身的 iOS 27.0 SDK 編譯(otool:minos 27.0、sdk 27.0):闔上window=(386.0, 678.0),展開(871.0, 669.0)。標記為sdk 27.2的 DuoProbe2:兩種姿態下的輸出都與 27.1 標記相同。simctl list runtimes -j列出 27.1 執行環境的supportedDeviceTypes只有一筆,即 iPhone Duo;在該執行環境上以 iPhone 18 Pro Max 類型執行simctl create會以「Incompatible device」失敗。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Kiradex 是作者的 App(941 Apps),版本 1.0、build 5,於 2026 年 10 月 2 日上傳至 TestFlight,App Store Connect 當天將其列為有效。專案事實取自該建置的原始碼:部署目標 iOS 27.0,以 27.1 SDK 建置(對 App 二進位檔執行
otool -l:minos 27.0、sdk 27.1),帶有顏色的UILaunchScreen,直向與兩種橫向,一個有五個分頁的TabView,以及兩處#available(iOS 27.1, *),一處包住ArrangementView,一處包住onHingeChange。路線圖那一行引自專案自己的筆記。走查是一個 UI 測試,在八種畫面停下,並請主機端指令稿以simctl io <udid> screenshot --display=primary與--display=primary-1擷取模擬器;它在 iPhone Duo 模擬器(iOS 27.1 執行環境)上以六種姿態執行,其中五種走完全部八站,闔上並旋轉時為七站,另外也在 iPhone 18 Pro Max 模擬器(iOS 27.0 執行環境,直向與橫向)上執行。擷取尺寸:1398 × 2034 與 2034 × 1398(外側),2853 × 2007 與 2007 × 2853(內側),1320 × 2868(6.9 吋)。檢視面板的左緣從內側螢幕擷取圖量得,攤平時位於 x 433.7,Book 姿態時位於 495.7。展開測試在闔上狀態啟動 App,持續擷取內側螢幕,再按下 Open;連續四個畫格顯示了開場動畫。保留卡片的測試在闔上時開啟一張卡片,按下 Open,二十秒後擷取。檢視器測試在 6.9 吋模擬器上於同一個工作階段內開啟全螢幕檢視器四次,並量測每張擷取圖:第一張有 52% 的非黑色像素,其餘三張為 0.3%。在 Xcode 27.1 beta 模擬器平台中的XCTest與XCUIAutomation框架裡,以文字搜尋「hinge」、「posture」、「DevicePose」與「foldState」,一無所獲,simctl也沒有列出任何姿態指令。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
作者於 2026 年 10 月 2 日的測試。一個在
if #available(iOS 27.1, *)之後使用ArrangementView的檔案,以swiftc -typecheck針對每個已安裝 Xcode 的模擬器 SDK 進行型別檢查:Xcode 27.0(27A266a)以error: cannot find 'ArrangementView' in scope失敗;Xcode 27.1 beta(27A9269)與 Xcode 27.2 beta(27B5019j)通過。同一個檔案以#if DUO_SDK圍起後,在 27.0 下不加旗標時通過,在 27.1 beta 下加或不加旗標都通過,在 27.0 下加上-D DUO_SDK則失敗。以#if canImport(SwiftUI, _version: 8.0.85)圍起後,三者都通過,而每個分支中的#warning顯示 27.0 編譯的是後備分支,兩個 beta 編譯的是 27.1 分支。xcrun swift --version在三者下都印出Apple Swift version 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1)。SwiftUI 模組版本是各 SDK 的SwiftUI.swiftinterface中的-user-module-version值。這裡安裝的 27.2 beta 是 beta 1(27B5019j);它的 iOS 27.2 模擬器執行環境(24B5084k)沒有列出 iPhone Duo 裝置類型,而 Apple 的 27.2 說明要開發者改用 27.1 beta 取得它。這個條件的語法出自 The Swift Programming Language 的 Statements,「Conditional Compilation Block」,於 2026 年 10 月 2 日取得:「platform-condition →canImport(import-path)」。 ↩↩↩↩↩↩↩↩↩ -
Xcode 27.1 beta(27A9269),
Contents/PlugIns/IDEIntelligenceChat.framework/Versions/A/Resources/:app-resizability.idechatprompttemplate以及app-resizability-ref-uiscreen-task.md.packaged、-orientation-task、-scene-lifecycle-task、-safe-area-task與-idiom-task,於 2026 年 10 月 2 日讀取;引文出自範本的「When to Use」一節,以及安全區域參考檔的第 6 條規則;三項專案檢查是它的「Prerequisites」表格,五種模式則是它的「Task Registry」。Xcode 27.0(27A266a)在同一個資料夾中有uikit-app-modernization.idechatprompttemplate與四個參考檔。 ↩↩↩ -
Apple,2026 年 10 月 2 日透過 JSON 端點取得的文件:ArrangementView、reservedRegions(kind:options:layoutDirectionBehavior:)、onHingeChange(isEnabled:_:)、toolbarVerticalBehavior(_:) 與 toolbarVerticalEdge。 ↩
-
Antoine van der Lee,iPhone Duo Simulator: Testing and optimizing your SwiftUI app,SwiftLee,2026 年 9 月 22 日,引用。Artem Novichkov,iPhone Duo by Examples,GitHub README,「Good to Know」,於 2026 年 10 月 2 日取得:「Its frame is the same in both states: 40 pt wide, with 20 pt margins on each side of a zero-width fold line.」、「Reserved regions arrive after the first layout pass.」與「When folded, the outer display has no reserved regions at all, not even inactive ones.」。前兩項與這裡的探測 App 一致;第三項則不一致,因為探測 App 在闔上的外側螢幕上讀到了兩個作用中的遮蔽區域。Mick MacCallum,How to Get Your App Ready for iPhone Duo,BleepingSwift,2026 年 9 月 18 日。Yurii Kleimenov,How to adapt your iOS app to iPhone Duo,Adapty,2026 年 9 月 11 日發布,標示於 9 月 15 日更新。 ↩↩↩↩↩
-
Apple,「App Store submissions now open for the latest OS releases」,開發者新聞,2026 年 9 月 9 日:自 2027 年 4 月起,上傳至 App Store Connect 的 App「need to meet the following minimum requirements」(必須符合下列最低要求),其中第一項是「iOS and iPadOS apps must be built with the iOS 27 & iPadOS 27 SDK or later」。 ↩
-
Apple,Get ready for iPhone Duo,於 2026 年 10 月 2 日取得:六場 Tech Talk、兩段 Group Lab 錄影、Group Lab 問答的連結、Photos and Camera、SwiftUI 與 UIKit 的論壇問答、Xcode 27.1 beta、設計指引與資源、準備指南,以及實體工作坊。Group Lab 問答頁面與論壇討論串未讀;對它們的請求都回傳了人機驗證頁面。 ↩↩↩
-
Apple,Xcode 27.1 Beta Release Notes,2026 年 10 月 2 日透過文件 JSON 端點取得,Simulator、Known Issues:「StandBy is unavailable in the iPhone Duo Simulator runtime. (187708663)」與「Running and debugging most app extensions is unavailable in the iPhone Duo Simulator runtime. (187708767)」。Apple,Xcode 27.2 Beta 2 Release Notes,於 2026 年 10 月 2 日以同樣方式取得:Overview,「Download Xcode 27.1 beta to get the iOS SDK and simulator support for iPhone Duo.」;General、Known Issues,187146039,引用。 ↩