iPhone Duo 发布后,同行问我的第一个问题是:"折叠角度能读吗?"能。Apple 同时提供铰链的离散状态和连续角度——SwiftUI 和 UIKit 都有。但就在演示这个 API 的那场 Tech Talk 里,也给出了一条硬性限制:铰链是用来做效果和交互的,不是用来做 layout 的。这个区别是根本性的,新 API 的整个后半部分都建立在它之上。我按顺序拆解:hinge API、reserved regions、arrangements 和相机——哪些 Objective-C 能直接用,哪些必须上 Swift。
和本系列第一篇一样先提醒:签名取自 Tech Talk 111463–111465,Xcode 27.1 仍在 beta——上生产前请与 SDK 头文件核对。
onHingeChange:三种状态和实时角度#
SwiftUI 修饰符 onHingeChange 会以前一个和当前的 context 调用闭包。Apple 的经典示例是一个吉他应用,折叠角度充当颤音摇杆:
struct InstrumentView: View {
/// 0 — no bend, 1 — maximum
@State private var pitchBend: Double = 0
var body: some View {
GuitarView(pitchBend: pitchBend)
.onHingeChange { _, context in
if let hinge = context.hinge, hinge.status == .partiallyOpen {
pitchBend = calculatePitchBend(angle: hinge.angle)
} else {
pitchBend = 0
}
}
}
}这里有两个容易漏掉的必要元素:
context.hinge是 Optional。nil表示设备没有铰链——同一个二进制也要跑在普通 iPhone 上。不做 guard,代码会在世界上大多数设备上崩掉。- 带重置的
else分支。 状态一共三种:.closed、.partiallyOpen、.fullyOpen。如果只在.closed时重置效果,那么设备完全展开摊平时,效果会"卡"在最后一个角度上。
UIKit 里同样的事情由 UIHingeInteraction 完成——按照 UIInteraction 家族的惯例,它被添加到 view 上,这也意味着 Objective-C 可以调用:
// API shape follows the UIInteraction family; verify the exact
// handler signature against the iOS 27.1 SDK headers
UIHingeInteraction *hinge = [[UIHingeInteraction alloc] init];
[self.effectView addInteraction:hinge];同样重要的是这里没有什么:通知。没有任何官方来源提到铰链相关的 NSNotification——观察只能通过 interaction 或修饰符。如果你在哪儿看到 UIDeviceHingeDidChangeNotification——那是编出来的。
为什么铰链不能驱动 layout#
诱惑显而易见:读角度,然后按角度排布界面。Apple 明说不要这么做。角度是连续的传感器数据流;绑在它上面的 layout 会在折叠过程中不停抖动,并且在所有没有铰链的设备上直接失效。结构性的决策有两个独立的 API 负责。
Reserved regions(iOS 27.1)——view 中被硬件"占用"的区域。分两类:.division 是折痕,会分割区域(只在设备半折叠时活跃;摊平状态下该区域不活跃且宽度为零);.occlusion 是摄像头,会遮挡区域。
// SwiftUI — from GeometryProxy
let folds = proxy.reservedRegions(kind: .division)
let cameras = proxy.reservedRegions(kind: .occlusion)
// For stable decisions: account for the fold even when inactive
let allFolds = proxy.reservedRegions(kind: .division, options: .includeInactive)// Objective-C: same method on UIView, regions are UIViewReservedRegion
NSArray<UIViewReservedRegion *> *folds =
[self.view reservedRegionsWithKind:UIViewReservedRegionKindDivision
options:0];
for (UIViewReservedRegion *region in folds) {
// region.frame — the fold rectangle in view coordinates
}.includeInactive 选项解决一个不那么显眼的问题:如果网格在折痕区域出现和消失时都重排一次,界面就会随铰链的每次动作"洗牌"。稳定的做法是——比如在任何带折痕的设备上始终使用偶数列。
好消息:系统容器自带免费的 fold avoidance。NavigationSplitView、TabView、sheet、alert、菜单和 popover 会自己把可交互元素挪出折痕的弯曲区域。唯一的例外是可滚动内容:文章或信息流允许从折痕下方流过,不需要挪动它。
ArrangementView:split 还是 overlay#
针对"介于导航和内容之间"的一对 view,出现了专门的容器。SwiftUI 里是 ArrangementView,UIKit 里是 UIArrangementViewController:
ArrangementView {
PlayerView()
} secondary: {
UpNextView()
}
.arrangementViewStyle(.split)UIArrangementViewController *arrangement = [[UIArrangementViewController alloc] init];
[arrangement setViewController:playerVC forPlacement:UIArrangementPlacementPrimary];
[arrangement setViewController:upNextVC forPlacement:UIArrangementPlacementSecondary];两种风格,语义不同。Split 分割 bounds、互不遮挡——播放器配转录文本那种 main/detail;相比 iPad 的行为还有个贴心的细节:关闭 secondary 时,primary view 不会横穿整块屏幕去重新居中,而是停靠在折痕旁。Overlay 是一个堆叠,折叠时展开成并排——用于覆盖在可滚动内容之上的前景控件。迁移的判断法则很简单:原来是 HStack/VStack 的——用 split;原来是 ZStack 的——用 overlay。
以及 Tech Talk 里的两条禁令:不要把 NavigationSplitView 放进 arrangement(这个容器不提供导航基础设施),也不要把 arrangement 放进 List/ScrollView。需要可折叠的多栏——那依然是 UISplitViewController 的活,arrangement 不是干这个的。
相机:position == .front 不再等于"正对着你"#
Duo 是第一台有两个前置摄像头的 iPhone:外置(4K/120)和内置屏下(1080p/60)。两者都如实上报 position == .front——但两块屏幕可能朝向相反,所以"前置"摄像头未必看着用户。2026 年之前写的所有镜像逻辑,都栽在这个事实上。
简单的路线是虚拟前置摄像头:一个系统设备,开合设备时自动在内外模组之间切换。发现方式一如既往(AVCaptureDeviceDiscoverySession 加 position .front——Objective-C 也能用),但它只提供两颗摄像头能力的交集:1080p、60 fps、没有 depth。对大多数应用来说这就够了,而且模组切换不归你管。
如果需要某个具体模组的完整能力,新增了 .builtInInnerUltraWideCamera 和 .builtInOuterUltraWideCamera 两种类型——但这时切换就是你的事了,于是登场的是最重要的新工具——AVKit 里的 AVCaptureDeviceDirectionCoordinator:
let coordinator = AVCaptureDeviceDirectionCoordinator(
view: previewView,
deviceTypes: [.builtInInnerUltraWideCamera, .builtInOuterUltraWideCamera]
) { descriptor in
// The descriptor is Sendable; build the AVCaptureDevice on the camera actor
Task { await cameraActor.switchTo(descriptor) }
}Coordinator 绑定到一个具体的 view,回答的问题是"此刻哪颗摄像头和这块显示屏朝向同一方向"。它隔离在 main actor 上,跨 actor 边界传递的不是 AVCaptureDevice,而是 AVCaptureDeviceDescriptor——设备的 sendable 表示。change 处理器里有三项职责:重新配置 session、按朝向而不是按 position 重新计算镜像、更新 UI。
Tech Talk 111465 里还有两个相机细节:迁移到 AVCaptureDeviceRotationCoordinator 之后要关掉 isCameraSensorOrientationCompensationEnabled——Duo 的前置摄像头默认开启补偿,不关就是双重工作;而 dynamicAspectRatio 可以让方形传感器为内屏的横屏模式服务。
第二块屏上的提词器#
相机类应用还多了一项能力:当主 UI 占据内屏并运行相机 session 时,可以往外屏输出附加内容——CameraCaptureAccessory。演讲里给的场景:录视频时的提词器,或者给正被拍照的小朋友看点好玩的。
CameraView(model: model)
.sceneAccessory {
CameraCaptureAccessory(isEnabled: $model.isEnabled) {
TeleprompterView(model: model)
}
.onAvailabilityChange { model.isAvailable = $0 }
}可用性由系统掌控,所以 onAvailabilityChange 不是可选项而是义务:用户随时可能把设备合上,accessory 随时可能消失。
小结#
新 API 的逻辑拼成了一幅完整的图:铰链是做效果的传感器,reserved regions 是供 layout 使用的硬件事实,arrangements 是一对 view 的现成模式,相机朝向是独立于 position 的实体。UIKit/AVFoundation 的全部表面 Objective-C 都能触达;SwiftUI 独占的部分(onHingeChange、ArrangementView、sceneAccessory)在 ObjC 项目里要么用 UIKit 等价物,要么架一层薄薄的 Swift——这就是本系列第三篇的主题。



