在 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 上亮相、未更新的游戏在展开的屏幕上是什么样子,以及开发者正在用铰链、半折和两块屏幕做出哪些新玩法。