SwiftUI互動iPhone Duo

在 iPhone Duo 上把轉軸當控制元件

用 onHingeChange 讀取轉軸,再把角度變成位置、速度或一次快速開合。開發者如何做出轉軸遊戲、他們準備的備用操作方式,以及如何在沒有真機的情況下測試。

2026年10月4日閱讀約 7 分鐘13 個來源

開啟 iPhone Duo,牆紙會隨著轉軸的轉動縮放。Apple 的多螢幕講座用一個吉他 App 做了同樣的事:你一折疊,音高就隨之彎曲。而開發者們搶先一步:創意牆上已經有一架手風琴、兩款 Flappy Bird、一款切割遊戲和一個物理實驗室,全都靠轉軸來玩。本文介紹他們是怎麼讀取轉軸的,以及從中學到了什麼。

在一切開始之前先記住一條規則:角度不適合用作佈局的輸入。要讓控制元件避開摺痕,請使用摺痕指南裡的保留區域和 arrangement。

要點

  • onHingeChange 給你一個狀態(合上、半開、完全展開)和一個從 0° 到 180° 的角度。它需要 iOS 27.1,在不能摺疊的手機上為 nil。
  • 在某個姿態下發生的事,就響應狀態。隨轉軸而動的東西,才用角度。
  • 把角度讀作位置、速度或一次快速開合。速度要對照時鐘來測量;快速開合要從一個移動的錨點開始檢測,並設定重新待命的條件。
  • 更新頻率和精度由系統決定。不要做任何依賴它們的東西。
  • 每款轉軸遊戲都提供了第二種操作方式:點按、輕掃或滑塊。

這個 API 給了你什麼

SwiftUI 有 onHingeChange,UIKit 有 UIHingeInteraction。兩者都報告一個狀態(.closed、.partiallyOpen 或 .fullyOpen)和一個連續的角度,合上為 0°,完全平放為 180°。

iPhone Duo 轉軸特寫:手機半開,從轉軸一側看去
來自 Apple 發布公告的轉軸。API 報告的角度從合上的 0° 到平放的 180°,頻率和精度由系統決定。圖片:Apple
.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 把轉軸速度(每秒多少度)轉換成氣壓。空氣會自己漏掉,所以你一停下,聲音就會消失。

Artem Novichkov@iosartem

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 有一個“自動送氣”開關,讓風箱一直保持充滿。

雙手捧著半開的 iPhone Duo,主畫面鋪滿內螢幕
轉軸遊戲的握持方式:雙手捧著,手機半開。圖片:Apple

當輸入就是整臺手機時,小小的確認回饋很有幫助。在 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 日。

繼續閱讀

App · 規格閱讀約 4 分鐘

iPhone Duo 能執行 iPad App 嗎?iPhone Duo 與 iPad mini

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

10月6日閱讀指南