feat(ipad): W1-W3 — adaptive split-view layout + finding fixes
T-iPad-2: AdaptiveRootView/LayoutPolicy (sole size-class decision), SplitRootView (NavigationSplitView sidebar+detail), StackRootView (iPhone path verbatim, zero regression); privacy shade hoisted to shared ZStack top for both branches T-iPad-3: KeyBarVisibility predicate (hide when hardware keyboard present), pointer context menu (copy/new-in-cwd/kill, all via existing channels) T-iPad-4: Projects multi-column grid on iPad (idiom-gated), adaptive sheet detents T-iPad-5 findings (4/4 fixed): kill onKillSession thread-through + AppCoordinator.killCurrentSession; split route-gated to .sessions (iPad first-run pairing); continue-last banner in split sidebar; iPad XCUITest deferred (covered by SidebarSelectionTests) Verified: iPhone 16 277 + iPad Pro 11 278 tests green; packages 261 + integration 10; zero changes under ios/Packages, src/, public/
This commit is contained in:
73
ios/App/WebTerm/Screens/ProjectsLayout.swift
Normal file
73
ios/App/WebTerm/Screens/ProjectsLayout.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
/// T-iPad-4 · Projects 大屏化的**纯布局决策** —— 列数 / 是否走网格 / sheet
|
||||
/// detents 全部收在本文件的纯函数里(单一判据点,仿 `LayoutPolicy` 先例),
|
||||
/// 视图内零散落条件、100% 单测。
|
||||
///
|
||||
/// **为何按 `UIUserInterfaceIdiom` 而非 `horizontalSizeClass`**(关键、易错):
|
||||
/// iPad 上 Projects 以**表单 sheet**(form sheet)呈现,其内部
|
||||
/// `horizontalSizeClass` 恒为 **compact**(与 iPhone 横屏完全相同)。因此
|
||||
/// size class 无法在「iPhone 横屏零回归」的前提下区分「iPhone」与「iPad 的
|
||||
/// Projects 表单 sheet」—— 只有设备 idiom 能。故 Projects 自身的多列/卡片
|
||||
/// 决策以 idiom+宽度为判据;根视图的 stack/split 决策仍由 `LayoutPolicy`
|
||||
/// (唯一 size-class 读取点)负责,两者正交、各自单点。
|
||||
enum ProjectsGridLayout {
|
||||
/// iPhone / 极窄 iPad 的回退列数(现有单列布局)。
|
||||
static let singleColumn = 1
|
||||
/// iPad 下升到 2 列的最小可用宽度(表单 sheet 内宽 ~440–540pt 即两列)。
|
||||
static let twoColumnMinWidth: CGFloat = 400
|
||||
/// iPad 下升到 3 列的最小可用宽度(更宽的面板/横屏全宽时)。
|
||||
static let threeColumnMinWidth: CGFloat = 760
|
||||
|
||||
/// iPhone(`.phone`,任何朝向/尺寸)→ 恒 `singleColumn`(现有单列 List,
|
||||
/// 字节级不变)。iPad(`.pad`)→ 按**可用宽度** 1–3 列。恒 `>= 1`(永不
|
||||
/// 返回 0,防空网格)。
|
||||
static func columnCount(
|
||||
availableWidth: CGFloat,
|
||||
idiom: UIUserInterfaceIdiom
|
||||
) -> Int {
|
||||
guard idiom == .pad else { return singleColumn }
|
||||
if availableWidth >= threeColumnMinWidth { return 3 }
|
||||
if availableWidth >= twoColumnMinWidth { return 2 }
|
||||
return singleColumn
|
||||
}
|
||||
|
||||
/// 容器选择:iPad → 多列网格;其它(iPhone)→ 现有单列 List(零回归)。
|
||||
static func usesGrid(idiom: UIUserInterfaceIdiom) -> Bool {
|
||||
idiom == .pad
|
||||
}
|
||||
}
|
||||
|
||||
/// T-iPad-4 · Projects sheet 的自适应 detents 决策(同一 idiom 判据)。
|
||||
enum ProjectsSheetSizing {
|
||||
/// iPhone → `nil`:**不套 `.presentationDetents`**,保持现有默认全高 sheet
|
||||
/// (iPhone 字节级不变)。iPad → 卡片式 `[.medium, .large]`:不铺满大屏,
|
||||
/// 仍可上拉到大。
|
||||
static func detents(idiom: UIUserInterfaceIdiom) -> Set<PresentationDetent>? {
|
||||
idiom == .pad ? [.medium, .large] : nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 应用点(把「iPhone 不套、iPad 套 detents」收进一个 modifier)
|
||||
|
||||
/// 条件套 `.presentationDetents` 的 modifier —— nil 时 `content` 原样透传,
|
||||
/// 故 iPhone 分支不引入任何 sheet 尺寸修饰符(零回归)。
|
||||
private struct AdaptiveSheetDetents: ViewModifier {
|
||||
let detents: Set<PresentationDetent>?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if let detents {
|
||||
content.presentationDetents(detents)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// iPhone 透传(现有全高 sheet,字节级不变);iPad 套卡片式 detents。
|
||||
func adaptiveProjectsSheetDetents(idiom: UIUserInterfaceIdiom) -> some View {
|
||||
modifier(AdaptiveSheetDetents(detents: ProjectsSheetSizing.detents(idiom: idiom)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user