大熊猫侯佩头像
关注
iPhone Duo 从外屏到内屏:ArrangementView 抢先适配封面图

iPhone Duo 从外屏到内屏:ArrangementView 抢先适配

在这里插入图片描述

前言

同一款播放器,在窄窗口里可能需要上下排列画面和队列;获得更宽空间时,可以让它们左右并排。到了折叠设备,问题又多了一层:内屏即使尺寸没有明显变化,中央的折叠区域也可能影响控件应该待在哪里。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ArrangementView 处理的正是两块相关内容如何共同适应环境的问题。理解它,不能只比较三个 API 名称,还要看同一份内容在外屏、全展开内屏、半展开内屏中的位置、尺寸和显示方式。

本文以 Apple 的 Strike a pose with adaptive layouts on iPhone Duo 为线索,使用一个风景播放器演示:蓝色 A 始终代表风景内容,橙色 B 始终代表队列或控制台。 两块区域的颜色和业务身份保持稳定,布局变化因而更容易观察。

更新日期:2026 年 9 月 25 日。本文对应当前 ArrangementViewTest 工程的新界面,使用 Xcode 27.1(27A9269)编译。外屏三种模式已有运行截图;内屏全展开、半展开及旋转尚未完成实测。后文会分别说明实现、已观察结果和待验证预期。

一、ArrangementView 负责两块内容的关系

ArrangementView 接收 primary 与 secondary。系统根据可用空间、Size Class 和硬件环境决定如何安排内容。默认样式为 .automatic,当前解析为 split arrangement;官方文档标注 iOS / iPadOS 27.1 起可用。ArrangementView 文档

本文把职责分成三层:

层级本例中的工作
ArrangementView决定 A/B 的分配区域、排列和显示行为
面板内部视图在分配到的区域中绘制风景、滚动队列、显示控制条
实验说明与诊断展示尺寸、活动分隔区域和当前模式的观察目标

这意味着,本文没有用“宽度超过某个数就创建 HStack”的方式冒充 ArrangementView,也没有用按钮模拟折叠后再手动摆放两个面板。三种模式切换的是 arrangement 策略;外屏、内屏和折叠姿态则由真实运行环境提供。

在这里插入图片描述

如果业务需要自行测量和放置任意数量的子视图,仍可以实现 Layout。它提供的是更底层的几何布局能力。Layout 文档

二、先认识三个演示,避免“切了模式却不知道在看什么”

新版界面用风景画面与队列形成明显对照。风景由 SwiftUI 的渐变和 Path 绘制,没有图片资源、网络服务或第三方依赖。播放按钮只改变 UI 状态,不播放真实音频。

页面选项使用的样式重点观察
双面板.split蓝色 A 与橙色 B 如何上下、左右重排
主辅切换.split.axes(.horizontal)不允许上下分割时,B 是否退让,A 是否获得更多空间
浮动控制台.overlayB 是叠在 A 上的控制条,还是获得独立区域的完整控制台

页面顶部始终保留队列入口。即使 B 暂时不可见,也能在弹窗中选择内容。playing 和 selectedTrack 保存在 arrangement 外部,模式切换不会主动创建一套新的播放状态。

在这里插入图片描述

这里还要区分业务身份和 API 角色:

模式primarysecondary
双面板、主辅切换A:风景播放器B:播放队列
浮动控制台B:前景控制台A:背景风景

A/B 标识用于帮助读者追踪同一块内容,并不固定等同于 primary/secondary。尤其在 overlay 中,前景控制台才是 primary。overlay 文档

三、双面板:看系统如何重新分配空间

当前工程中的核心代码如下,ScenicPane 和 TrackPane 的完整实现见文末:

ArrangementView {
    ScenicPane(mode: mode, selectedTrack: selectedTrack, playing: $playing)
        .measurePane("A")
} secondary: {
    TrackPane(selectedTrack: $selectedTrack)
        .measurePane("B")
}
.arrangementViewStyle(.split)

在官方视频的演示里,容器偏宽时左右分割,偏高时上下分割;实际决策还受到环境影响。视频:12:00 起

本次外屏实测中,A 和 B 上下排列,各自获得 382 × 211 pt。这次显示的是每块面板自己的 GeometryReader 尺寸,而不是包含标题、说明文字在内的整个页面高度。

在这里插入图片描述

到了全展开内屏,重点应是“每块内容现在获得了多少空间、排列是否变化”。不能预设内屏一定左右排列:旋转、分屏、系统栏以及页面自己的说明区域都会改变 arrangement 的可用比例。

半展开时,还要观察面板边界与活动 division 的关系。不要仅凭截图更宽,就判断折叠适配已经发生。 比较时应同时记录运行姿态、活动区域及面板尺寸。

容器自适应,不代表面板内容可以固定尺寸

新版风景按面板宽高绘制,队列则在 B 内部滚动。因此,当 A 被压缩成横向短面板、B 的高度减少时,内容仍能利用自己的区域。ArrangementView 本身位于导航容器内部,而不是被包进滚动列表;滚动发生在面板内部。视频:容器使用边界

在这里插入图片描述

如果内容有明确约束,可以在相应面板上设置 splitArrangementLayoutSize 或 splitArrangementLayoutRatio。本例不添加比例约束,以便先观察系统的默认分配。比例偏好还会受到优先级和剩余空间影响,不能理解成每种姿态下都严格成立的固定比例。比例 API 文档

四、主辅切换:限制方向,观察 B 如何退让

第二个模式只改变允许的分割方向:

ArrangementView {
    ScenicPane(mode: mode, selectedTrack: selectedTrack, playing: $playing)
        .measurePane("A")
} secondary: {
    TrackPane(selectedTrack: $selectedTrack)
        .measurePane("B")
}
.arrangementViewStyle(.split.axes(.horizontal))

它表达的是“允许水平分割”,并不意味着空间不足时仍必须挤出两列。官方视频中,当主方向为纵向而样式只允许水平分割时,只显示主内容。视频:12:41

在这里插入图片描述

本次外屏结果非常直观:橙色 B 不再显示,蓝色 A 从 382 × 211 pt 扩大到 382 × 422 pt。风景在同一窗口里获得了双倍高度,读者可以直接看到“限制方向”的结果。

在这里插入图片描述

接着应在全展开内屏和旋转后检查 B 是否重新出现。这里的测试对象是系统分配结果,不是自行维护一个 isDuoExpanded 布尔值来隐藏队列。

B 的退让也解释了为什么保留独立队列入口很重要:布局可以收起一块面板,但不应同时删除用户选择内容的能力。

五、浮动控制台:从叠放到独立区域

第三个模式把橙色控制台设为 primary,把蓝色风景设为 secondary:

ArrangementView {
    AdaptiveConsole(
        compact: panesOverlap,
        selectedTrack: $selectedTrack,
        playing: $playing
    )
    .measurePane("B")
    .overlayArrangementEdge(HorizontalEdge.trailing)
    .overlayArrangementEdge(VerticalEdge.bottom)
} secondary: {
    ScenicPane(mode: mode, selectedTrack: selectedTrack, playing: $playing)
        .measurePane("A")
}
.arrangementViewStyle(.overlay)

overlay 可以先让内容叠放,再在环境变化时将内容分开排列。边缘修饰符指定转为相应方向布局时的位置偏好;这里明确区分水平 trailing 和垂直 bottom 两个重载。ArrangementView 文档 · 边缘修饰符

在本次外屏截图中,A 占据背景,B 是带橙色边框的浮动控制条。它与“主辅切换”不同:B 没有消失,而是与 A 使用重叠的空间。

在这里插入图片描述

当系统将两个分配区域分开时,新版代码会把 B 展开为独立控制台:显示实际尺寸、完整可滚动队列、播放按钮和下一段按钮。这个分离分支已经实现并通过编译,尚未取得半展开内屏的运行截图,不能写成已经实测成功。

官方视频使用环境值;本例观测实际几何

视频通过 overlayArrangementZIndex 调整内容的紧凑状态。这个环境值描述 arrangement 中的叠放层级,不是设备折叠角度。环境值文档

早期实验里,不同读取层级曾出现不同读数;用户后续截图中的 splitArrangementAxis 也与先前运行观察不同。这些观察不足以证明 API 存在通用缺陷,更不能建立“某个 axis 值就代表两个面板都可见”的规则。新版因此不再把 axis/z-index 直接作为页面上的布局结论。

在这里插入图片描述

本例采用另一种可检查的方法:在同一命名坐标空间中测量 A/B 的分配矩形,判断两者是否相交。这个判断只改变控制台内部内容,不负责把 A/B 摆到哪里。

private struct PaneFrames: PreferenceKey {
    static var defaultValue: [String: CGRect] { [:] }

    static func reduce(
        value: inout [String: CGRect],
        nextValue: () -> [String: CGRect]
    ) {
        value.merge(nextValue(), uniquingKeysWith: { _, new in new })
    }
}

private extension View {
    func measurePane(_ name: String) -> some View {
        background {
            GeometryReader { proxy in
                Color.clear.preference(
                    key: PaneFrames.self,
                    value: [name: proxy.frame(in: .named("arrangementStage"))]
                )
            }
        }
    }
}

在 arrangement 外部建立坐标空间并接收观测结果:

private var stage: some View {
    arrangement
        .coordinateSpace(name: "arrangementStage")
        .onPreferenceChange(PaneFrames.self) { frames = $0 }
        .clipped()
}

然后计算重叠状态:

private var panesOverlap: Bool {
    guard let a = frames["A"], let b = frames["B"],
          a.width > 1, b.width > 1 else {
        return true
    }
    let intersection = a.intersection(b)
    return !intersection.isNull
        && intersection.width > 2
        && intersection.height > 2
}

首次布局尚未得到有效矩形时,默认采用紧凑控制条;两个矩形在两个方向上都有超过 2 pt 的交集时,视为重叠。2 pt 是这个演示的几何容差,不是 Apple 规定的折叠阈值。

控制台外层始终填满系统分配的区域,内部才在紧凑条与完整队列之间切换。这样可以减少“内容变大导致测量变大、测量变大又改变内容”的反馈。播放状态与选中项目保持在父视图中,不因内容分支切换而主动重置。

在这里插入图片描述

这个办法也有明确边界:矩形交集不是通用可见性检测,无法判断 opacity、复杂裁剪或遮挡顺序;过渡动画中还可能出现短暂相交。本例只用它区分两个 overlay 面板的分配关系,不用它推断设备姿态,也不用它判断主辅模式中 B 是否可见。推广到生产界面前,应验证动画、旋转和折叠全过程。

六、外屏、全展开、半展开:哪些信息能判断,哪些不能

新版页面会查询当前视图范围内的 division:

let regions = proxy.reservedRegions(
    kind: .division,
    options: .includeInactive,
    layoutDirectionBehavior: .fixed
)
let active = regions.filter(\.isActive)

.includeInactive 让示例显式纳入非活动区域,再通过 isActive 分类,避免依赖资料中存在差异的默认查询描述。includeInactive 文档

在这里插入图片描述

页面只给出与查询证据相符的说明:

查询结果页面显示不能据此推断的内容
存在活动 division活动折叠区域,观察两侧避让具体折叠角度和所有控件是否已避让
存在 division,但未激活折叠区域未激活,观察连续画布所有设备和窗口条件下都代表全展开
没有返回 division当前窗口没有折叠分隔区域一定是外屏、一定是普通 iPhone

因此,“外屏”“全展开内屏”“半展开内屏”是测试者在 Simulator 中设置并记录的场景,不是这段区域查询代码完整识别出的三种设备状态。

粉色标记只画出系统返回的区域

点击状态行右侧的标记按钮,可以开关活动 division 的粉色覆盖层。区域在实验舞台自己的 GeometryReader 中重新查询,避免把外层容器坐标直接拿来画在内部视图上。

这里使用 .fixed,按照返回的固定几何坐标绘制调试层;真正的内容布局仍由 ArrangementView 处理。粉色层禁用命中测试,不拦截按钮,也不会通过额外 padding 再避让一次铰链。需要自行实现 RTL 布局时,应仔细核对 API 的镜像语义。reservedRegions 文档

.division 与 .occlusion 也不能混为一谈:前者表示分隔,后者表示遮挡。当前粉色层只显示 division,不是摄像头轮廓,也不是所有安全区域的可视化。

七、摄像头遮挡修复:把入口放在可控的内容区域

此前的实际截图中,右上角工具栏的队列按钮与摄像头重叠。把工具栏项改成 .topBarLeading 后,系统仍可能根据 Duo 的布局规则将栏内容重新排列,不能只凭 placement 名称判断物理位置。

新版把队列入口放到模式选择器左侧,位于页面内容内部;弹窗的“完成”按钮则放在底部安全区域:

.safeAreaInset(edge: .bottom) {
    Button("完成") { dismiss() }
        .buttonStyle(.borderedProminent)
        .frame(maxWidth: .infinity, minHeight: 44)
        .padding(8)
        .background(.bar)
}

这是一项针对演示界面的布局修正,不是自行探测摄像头后计算位移的通用方案。当前外屏截图已检查入口位置,但所有内屏姿态、RTL 和大字号仍需继续验证。不要把它扩展成“工具栏都不应该使用”的结论。

在这里插入图片描述

Apple 的设计指导强调可调整尺寸、边距与安全区域,以及不同姿态下的功能连续性;本文保留独立队列入口也是出于这个目的。Design for iPhone Duo

八、验证记录与接下来的截图矩阵

当前 ArrangementViewTest 已用 Xcode 27.1 编译成功,并在 Duo / iOS 27.1(24A94401)运行。构建日志保存在配套目录的 evidence/current-project-build.log 中。现有 AppIntents 警告表示未依赖相应框架而跳过元数据提取,与本例布局功能无关。

场景本次状态证据或待观察内容
外屏,双面板已运行并截图A/B 上下排列,各 382 × 211 pt
外屏,主辅切换已运行并截图仅显示 A,382 × 422 pt
外屏,浮动控制台已运行并截图A 为 382 × 422 pt,B 是重叠控制条
全展开内屏,三种模式待验证A/B 尺寸与排列;覆盖模式是否仍叠放
半展开内屏,三种模式待验证division 活动标记、面板位置、控制台分离分支
内屏旋转、分屏、外内屏切换待验证页面重排与播放状态连续性
普通 iPhone / iOS 27.1未验证当前安装的 runtime 不支持创建相应对照设备

面板尺寸是这次截图的结果,不是实现中的断点,也不是对所有 Duo 的固定规格。两种姿态得到相同布局是可能的;示例不能为了“看起来有变化”而人为强制排列。

推荐的实际操作顺序

  1. 在外屏依次选择三种模式,对照本文截图,确认 A/B 的含义和变化。
  2. 将设备切到全展开内屏,保持同一模式和内容,记录面板尺寸、division 和布局。
  3. 让内屏部分折叠,重点查看粉色区域与面板边界;在覆盖模式中检查 B 是否从浮动条变成独立控制台。
  4. 旋转设备后重复,确认没有把“横屏”和“水平分割”混为一谈。
  5. 播放/暂停、选择另一段内容,再切换模式与姿态,检查业务状态是否保留。
  6. 最后测试大字号和弹窗,避免只验证静态画面。

不要通过调整 SwiftUI frame 或直接写入环境值冒充真实折叠测试。需要在 Simulator 的姿态控制中改变设备状态,再记录真实结果。

截图前先确认显示端口和目标设备:

/Applications/Xcode27.1.app/Contents/Developer/usr/bin/simctl io booted enumerate
/Applications/Xcode27.1.app/Contents/Developer/usr/bin/simctl io booted screenshot --display=1 screenshot.png

1 是本次外屏截图所用编号,内屏应以枚举结果为准。同时启动多台模拟器时,请将 booted 换成目标 UUID。截图说明应包含模式、外/内屏、姿态、运行时版本和区域信息。

九、工程结构与官方阅读路线

当前项目保留原有 App 入口:

  • ArrangementViewTestApp.swift:应用入口。
  • ContentView.swift:系统可用性检查与预览。
  • ArrangementLab.swift:三个演示、面板几何观测、区域标记和说明弹窗。

配套压缩包中的独立 ArrangementLab.xcodeproj 也已同步同一套实现,便于读者单独打开;其 Sources/ArrangementLabApp.swift 是上述三份文件的合并版本。两个工程择一运行即可。本文截图来自当前 ArrangementViewTest 工程。

建议在官方主视频中依次查看 6:39 区域查询、11:17 创建容器、13:21 覆盖布局,并与本文的外屏截图和内屏待验证矩阵对照。

API 详情可以继续查阅 SwiftUI 更新、样式修饰符、splitArrangementAxis 和 Xcode 27.1 发布说明。实现中的几何观测属于本文的演示逻辑,应与系统 API 的保证区分开。

那么,感谢宝子们的观赏,我们下次不见不散哦!😎

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/mydo/article/details/166643843

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--