feat(ios): W4 app wiring — DI graph, event fan-out, lifecycle, privacy shade
T-iOS-15: AppEnvironment production DI (real Keychain/probe/transports), EventFanOut (engine.events → TerminalVM + GateVM + activity bridge), scenePhase lifecycle (.background→close, suspend→rebuild+reopen reclaims size), privacy shade on != .active, spike screen deleted, cold-start routing Walkthrough proxies: hosted live-server smoke over the production DI graph (probe→store→ attach→echo→close) + real-Keychain kSecAttrAccessible assertion (closes T-iOS-7 deferral) Deviation closed: TerminalViewModel.lastSentDims (valid-only) + alive-engine .active → notifyForegrounded(dims:) (orchestrator fix on behalf of T-iOS-11 owner) Env finding: repo under ~/Documents → TCC blocks sim-spawned node; SimServerHarness dual-mode (self-bootstrap / WEBTERM_SERVER_URL via simctl launchctl setenv) Verified: 214 pkg + 76 app + 10 integration tests green; verify agent 8/8 PASS
This commit is contained in:
126
ios/App/WebTerm/Wiring/RootView.swift
Normal file
126
ios/App/WebTerm/Wiring/RootView.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
import HostRegistry
|
||||
import SwiftUI
|
||||
|
||||
/// T-iOS-15 · Root of the app: route switch (Pairing / SessionList), terminal
|
||||
/// push, add-host sheet, scenePhase forwarding and the privacy shade.
|
||||
///
|
||||
/// The shade is the TOPMOST layer of this ZStack and appears whenever
|
||||
/// `scenePhase != .active` (exact rule — PrivacyShadePolicy + tests); it
|
||||
/// covers the whole navigation tree, terminal included. Sheets live above any
|
||||
/// overlay, but no sheet renders terminal bytes (pairing / plan gate only).
|
||||
struct RootView: View {
|
||||
@Bindable var coordinator: AppCoordinator
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
NavigationStack {
|
||||
rootContent
|
||||
.navigationDestination(isPresented: terminalBinding) {
|
||||
terminalDestination
|
||||
}
|
||||
}
|
||||
if PrivacyShadePolicy.isShadeVisible(for: scenePhase) {
|
||||
PrivacyShadeView()
|
||||
}
|
||||
}
|
||||
.task { await coordinator.bootstrap() }
|
||||
.onChange(of: scenePhase) { _, phase in
|
||||
coordinator.handleScenePhase(phase)
|
||||
}
|
||||
.sheet(
|
||||
isPresented: $coordinator.isAddHostPresented,
|
||||
onDismiss: { coordinator.addHostDismissed() }
|
||||
) {
|
||||
addHostSheet
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Route switch
|
||||
|
||||
@ViewBuilder private var rootContent: some View {
|
||||
switch coordinator.route {
|
||||
case .loading:
|
||||
ProgressView()
|
||||
case .pairing:
|
||||
firstRunPairing
|
||||
case .sessions:
|
||||
sessionList
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private var firstRunPairing: some View {
|
||||
if let viewModel = coordinator.rootPairingViewModel {
|
||||
PairingScreen(viewModel: viewModel) { host in
|
||||
coordinator.completeFirstPairing(host)
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
|
||||
private var sessionList: some View {
|
||||
SessionListScreen(
|
||||
viewModel: coordinator.sessionList,
|
||||
onOpen: { coordinator.open($0) },
|
||||
onAddHost: { coordinator.presentAddHost() }
|
||||
)
|
||||
.safeAreaInset(edge: .bottom) { continueLastBanner }
|
||||
}
|
||||
|
||||
// MARK: - "继续上次" highlight (cold start step 5)
|
||||
|
||||
@ViewBuilder private var continueLastBanner: some View {
|
||||
if coordinator.continueLastSessionId != nil {
|
||||
Button {
|
||||
coordinator.openContinueLast()
|
||||
} label: {
|
||||
Label(RootCopy.continueLast, systemImage: "arrow.uturn.forward.circle.fill")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.padding(.horizontal, RootMetrics.bannerHorizontalPadding)
|
||||
.padding(.vertical, RootMetrics.bannerVerticalPadding)
|
||||
.background(.thinMaterial)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Terminal push
|
||||
|
||||
private var terminalBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { coordinator.terminalController != nil },
|
||||
set: { presented in
|
||||
guard !presented else { return }
|
||||
coordinator.closeTerminal() // back → engine.close() (detach)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder private var terminalDestination: some View {
|
||||
if let controller = coordinator.terminalController {
|
||||
TerminalContainerView(controller: controller)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Add-host sheet (multi-host entry, list header)
|
||||
|
||||
@ViewBuilder private var addHostSheet: some View {
|
||||
if let viewModel = coordinator.addHostPairingViewModel {
|
||||
NavigationStack {
|
||||
PairingScreen(viewModel: viewModel) { host in
|
||||
coordinator.completeAddHost(host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum RootMetrics {
|
||||
static let bannerHorizontalPadding: CGFloat = 16
|
||||
static let bannerVerticalPadding: CGFloat = 8
|
||||
}
|
||||
|
||||
private enum RootCopy {
|
||||
static let continueLast = "继续上次会话"
|
||||
}
|
||||
Reference in New Issue
Block a user