在 iPhone Duo 上把轉軸當控制元件
用 onHingeChange 讀取轉軸,再把角度變成位置、速度或一次快速開合。開發者如何做出轉軸遊戲、他們準備的備用操作方式,以及如何在沒有真機的情況下測試。
開啟 iPhone Duo,牆紙會隨著轉軸的轉動縮放。Apple 的多螢幕講座用一個吉他 App 做了同樣的事:你一折疊,音高就隨之彎曲。而開發者們搶先一步:創意牆上已經有一架手風琴、兩款 Flappy Bird、一款切割遊戲和一個物理實驗室,全都靠轉軸來玩。本文介紹他們是怎麼讀取轉軸的,以及從中學到了什麼。
在一切開始之前先記住一條規則:角度不適合用作佈局的輸入。要讓控制元件避開摺痕,請使用摺痕指南裡的保留區域和 arrangement。
要點
onHingeChange給你一個狀態(合上、半開、完全展開)和一個從 0° 到 180° 的角度。它需要 iOS 27.1,在不能摺疊的手機上為 nil。- 在某個姿態下發生的事,就響應狀態。隨轉軸而動的東西,才用角度。
- 把角度讀作位置、速度或一次快速開合。速度要對照時鐘來測量;快速開合要從一個移動的錨點開始檢測,並設定重新待命的條件。
- 更新頻率和精度由系統決定。不要做任何依賴它們的東西。
- 每款轉軸遊戲都提供了第二種操作方式:點按、輕掃或滑塊。
這個 API 給了你什麼
SwiftUI 有 onHingeChange,UIKit 有 UIHingeInteraction。兩者都報告一個狀態(.closed、.partiallyOpen 或 .fullyOpen)和一個連續的角度,合上為 0°,完全平放為 180°。

.onHingeChange { old, new in
guard let hinge = new.hinge else { return } // nil:沒有可讀取的轉軸
status = hinge.status
degrees = hinge.angle.degrees
}
Artem Novichkov 的 iPhone Duo by Examples 在 iOS 27.1 模擬器上把這個 API 的每一部分都試了一遍,這些細節值得了解:
- 處理程序會先以初始狀態執行一次,之後每次變化都會執行。
- 在沒有轉軸的裝置上,以及在檢視層級不提供轉軸更新的檢視中,
hinge為 nil。 DeviceHinge.Status是一個帶靜態成員的結構體,不是列舉,所以對它的switch需要default。onHingeChange(isEnabled:)可以暫停推送,而不必移除修飾符。關閉期間發生的變化之後也不會補發。- 更新多久來一次、度數有多精確,都由系統決定。不要依賴某個特定的頻率。
這個修飾符需要 iOS 27.1。Captain Touch 的 Folding Flappy 支援 iOS 17 及更高版本,所以他把呼叫包在一個在舊系統上什麼也不做的修飾符裡:
extension View {
func onHingeAngle(_ action: @escaping (Double?) -> Void) -> some View {
modifier(HingeAngle(action: action))
}
}
private struct HingeAngle: ViewModifier {
let action: (Double?) -> Void
func body(content: Content) -> some View {
if #available(iOS 27.1, *) {
content.onHingeChange { _, new in
action(new.hinge?.angle.degrees)
}
} else {
content
}
}
}
狀態用於時刻,角度用於動作
狀態回答的是“手機處於什麼姿態”。角度則不能回答這個問題,儘管看起來應該能。Ihor Malovanyi 把模擬器裡隱藏的轉軸滑塊撥了幾百次,記錄下返回的結果。拖動開啟時,狀態在大約 20° 時變成半開。往回拖時,一次在 27° 讀到合上,另一次在 46°。完全展開只在恰好 180° 時出現。如果改成點選滑塊,123° 停在合上,143° 停在半開。同一個角度可能對應兩種姿態。
ryo 的你比我猜遊戲 CharaDUO 展示了靠猜的代價。它根據角度範圍檢測桌面姿態,而這個範圍不得不從 75–115° 放寬到 70–150°,因為模擬器的桌面姿態報告的是 127.8°。它的 README 坦言,這個範圍是按模擬器調出來的,還需要把真機放到桌上試一試。
所以,凡是在某個姿態下發生的事,都去響應 status。Elvin 的抽認卡原型在手機折著時隱藏答案,開啟時顯示答案。Vidit Bhargava 的 Duo-Man 在手機展開時裝入磁帶,合上時開始播放。要區分書本式和筆電式,就讀取生效的分隔區域朝哪個方向延伸,見摺痕指南。角度留給隨轉軸而動的東西。
讀取角度的三種方式
創意牆上的每個轉軸創意,都是用以下三種方式之一來讀取角度的。
作為位置
角度本身就是數值。Ali Amjad 的團隊在 Bitrig 與 YC 聯合舉辦的駭客松上做出的 Lumi’s Lab 用它來教斯涅爾定律:轉軸角度就是入射光線的角度,學生開啟手機時,折射光線隨之更新。zunda 的 Pita-kaku 給你一個目標角度,讓你把手機折到那個角度。Steven Zhang 的 Crate 用它掃動一個低通濾波器。已經上架 App Store 的 Innards 會在你開啟手機時,讓錶殼漸漸變成玻璃,然後消失。
Apple 的吉他示例只在手機半開時把角度對映為彎音,在其他所有狀態下都把彎音歸零。你的數值也要做同樣的決定:手機合上、平放或根本報告不了轉軸時,它應該是多少。
作為速度
Artem 的 Accorduon 是一架手風琴,轉軸就是風箱。按住琴鍵時,風箱不動就不會出聲;折得越快,聲音越響越亮。這個 App 把轉軸速度(每秒多少度)轉換成氣壓。空氣會自己漏掉,所以你一停下,聲音就會消失。
Someone please stop me Meet Accorduon — an accordion for iPhone Duo. The hinge is the bellows: fold and unfold to play 🪗 (Sound on 🔊) github.com/artemnovichkov…
更新頻率由系統決定,這帶來兩個推論。速度要對照時鐘來測量,而不是按每次更新來算。另外,要用你自己的計時器讓數值衰減:轉軸停止移動時,更新也會停止,不會有任何東西提醒你該安靜下來了。
@State private var last: (degrees: Double, time: Date)?
.onHingeChange { _, new in
guard let degrees = new.hinge?.angle.degrees else {
last = nil
return
}
let now = Date.now
if let last {
let seconds = max(now.timeIntervalSince(last.time), 0.001)
// 每秒多少度,對照時鐘測量。
bellows.pump(abs(degrees - last.degrees) / seconds)
}
last = (degrees, now)
}
作為快速開合
轉軸變成了一個按鈕。三款遊戲各自獨立地做出了同樣的檢測器,它們的引數是個不錯的起點:
| 遊戲 | 觸發條件 | 重新待命條件 | 其他操作方式 |
|---|---|---|---|
| Artem 的 DuoBird | 從上次扇動以來的最低角度再開啟 10° | 轉軸往回折 5° | 點按 |
| 同樣出自 Artem 的 DuoCut | 從最高點合上 12° | 轉軸開啟 6° | 沿切割線輕掃 |
| Captain Touch 的 Folding Flappy | 任一方向 8° 的一次動作 | 方向反轉;朝同一方向繼續不算再扇一次 | 點按 |
它們有三個共同的思路。從一個移動的錨點開始測量,而不是從固定角度,這樣無論手機被拿成什麼角度,快速開合都能生效。要求最小的動作幅度,這樣感測器噪聲就不會誤觸發。只在方向反轉時重新待命,這樣一次長長的展開只算扇一下,而不是五下。
/// 每次快速展開扇一下,無論手機拿成什麼角度。
struct SnapDetector {
var trigger = 10.0 // 比最低角度多開啟這麼多就觸發
var rearm = 5.0 // 然後要往回折這麼多,才能觸發下一次
// 待命時記錄最低角度,等待重新待命時記錄最高角度。
private var anchor: Double?
private var armed = true
mutating func update(_ degrees: Double) -> Bool {
guard var point = anchor else {
anchor = degrees
return false
}
defer { anchor = point }
if armed {
point = min(point, degrees)
if degrees - point >= trigger {
armed = false
point = degrees
return true
}
} else {
point = max(point, degrees)
if point - degrees >= rearm {
armed = true
point = degrees
}
}
return false
}
}
@State private var snap = SnapDetector()
GameView(game: game)
.onHingeChange { _, new in
guard let degrees = new.hinge?.angle.degrees else { return }
if snap.update(degrees) { game.flap() }
}
如果要檢測快速合上,就像 DuoCut 那樣把比較方向反過來。
為一個需要雙手握持的慢速按鈕做設計
一次快速開合比一次點按耗時更長。DuoBird 相應地降低了重力、加寬了水管之間的空隙,而且從不要求你把手機完全開啟或用力合上。
要清楚手機移動時玩家在看哪裡。Captain Touch 注意到,半開時,模擬器會把內螢幕摺痕左側的那一半模糊並調暗,而且沒有公開 API 可以關掉這個效果。所以 Folding Flappy 把小鳥、分數和選單都放在摺痕右側,並通過分隔區域找到摺痕。DuoCut 用 .includeInactive 讀取同一塊區域,因為它的刀刃就是折線,玩家在手機還平放、正在對準切割位置時就需要看到它。
握持方式也要設計。Accorduon 像一本書一樣張開拿著:右手在豎向鍵盤上彈旋律,左手按貝斯按鈕,摺疊來拉動風箱。在模擬器裡,一個指標無法同時按住琴鍵又摺疊手機,所以這個 App 有一個“自動送氣”開關,讓風箱一直保持充滿。

當輸入就是整臺手機時,小小的確認回饋很有幫助。在 Artem 的 Duogami 裡,你合上手機時紙張隨之折起,同時伴有輕微的觸感回饋。
提供第二種操作方式
這些遊戲全都可以不折疊就玩。DuoBird 和 Folding Flappy 點按就能扇翅膀,DuoCut 可以沿切割線輕掃,Accorduon 可以拖動風箱或開啟自動送氣,Duogami 有一個滑塊和一個“摺疊”按鈕。這照顧到了不能摺疊的手機、模擬器的單一指標,以及那些沒法或不想在一局遊戲裡把手機折上幾百次的人。Apple 的設計講座對每個功能都有同樣的要求:不要把功能限制在某一種姿態。
Folding Flappy 還照顧到了沒有轉軸 API 的 iOS 27.0。摺疊會讓場景在兩塊螢幕之間移動,所以它的面積會發生很大變化。面積變化超過 25% 就算一次扇動,而旋轉不改變面積,就不算。
在模擬器上測試
- 在 Device Hub 的姿態按鈕上按住 Option 鍵,它們會變成一個從 0° 到 180° 的轉軸滑塊。這是 Jeremy Sequoia 發現的,Ihor 的實測筆記也有記錄。既要拖動它,也要點選它:兩者最後停留的狀態不同。
- “書本”姿態按鈕會停在 128° 附近。Blake Crosley 讀到的是 127°,CharaDUO 的桌面姿態報告的是 127.8°。確保你的邏輯沒有隻針對這一個數字調校。
- Artem 的
hinge命令可以從指令碼或程式設計智慧體中設定角度:hinge 120,或者hinge sweep 180 0 2,在兩秒內把手機合上。Artem 的示範裡,Claude Code 就是用它來玩 DuoBird 的。
- 像 Artem 的角度歷史示例那樣,在掃動過程中記錄每次更新,看看你的檢測器實際收到了什麼。然後確保沒有任何東西依賴那個頻率。
任何模擬器都無法告訴你這些在手裡是什麼感覺。Julian Schiavo 的彈球示範提出了一個問題:真正的轉軸是否結實到可以當擋板用?這個問題,以及一個人一次願意快速開合多少下,都要等到 10 月 23 日。
繼續閱讀

iPhone Duo App 創意:開發者正在用摺疊螢幕做的 6 件事
300 多個 iPhone Duo 示範、概念和原型,歸納為反覆出現的六種模式,從半折的筆記型電腦形態到把轉軸當控制器。如果你正在考慮做點什麼,可以從這裡開始。

iPhone Duo 能執行 iPad App 嗎?iPhone Duo 與 iPad mini
iPhone Duo 的內螢幕幾乎有 iPad mini 那麼大,但它執行的是 iOS,不是 iPadOS。這對僅限 iPad 的 App 意味著什麼、為什麼很多 Duo App 看起來會像它們的 iPad 版,以及 Duo 從 iPad 借來了什麼。

iPhone Duo 遊戲:即將到來的作品,以及摺疊螢幕給遊戲帶來了什麼
哪些遊戲已在 iPhone Duo 上亮相、未更新的遊戲在展開的螢幕上是什麼樣子,以及開發者正在用轉軸、半折和兩塊螢幕做出哪些新玩法。