diff --git a/apps/mobile/modules/t3-native-controls/ios/T3LayoutMetricsView.swift b/apps/mobile/modules/t3-native-controls/ios/T3LayoutMetricsView.swift index 4e0fcf6d2206b5feb1060e1bd3281f8a56343c6d..1cde09d65f61fa01929daa4d1f4db470865581cd 100644 --- a/apps/mobile/modules/t3-native-controls/ios/T3LayoutMetricsView.swift +++ b/apps/mobile/modules/t3-native-controls/ios/T3LayoutMetricsView.swift @@ -35,9 +35,21 @@ final class T3LayoutMetricsView: ExpoView { override func didMoveToWindow() { super.didMoveToWindow() lastMetrics = nil + assumeNoHingeIfSilent() publishMetrics() } + /// UIKit only sends hinge updates on devices that have a hinge. Without this, + /// an ordinary iPhone never publishes and the navigator never attaches. + private func assumeNoHingeIfSilent() { + guard window != nil, hasHinge == nil else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in + guard let self, self.window != nil, self.hasHinge == nil else { return } + self.hasHinge = false + self.publishMetrics() + } + } + override func layoutSubviews() { super.layoutSubviews() publishMetrics() diff --git a/apps/mobile/src/native/NativeWorkspaceColumns.ios.tsx b/apps/mobile/src/native/NativeWorkspaceColumns.ios.tsx index 9aee432ed28d140aca82d156d2d04f3842365d94..0f25bead0ae195b71efd74148812ac8ae528f2b9 100644 --- a/apps/mobile/src/native/NativeWorkspaceColumns.ios.tsx +++ b/apps/mobile/src/native/NativeWorkspaceColumns.ios.tsx @@ -1,4 +1,5 @@ import { requireOptionalNativeModule } from "expo"; +import { useEffect, useState } from "react"; import { Platform } from "react-native"; import { useNativeLayoutMetrics } from "./native-layout-metrics"; @@ -9,15 +10,27 @@ const nativeControls = requireOptionalNativeModule<{ export const NATIVE_WORKSPACE_COLUMNS_SUPPORTED = Platform.OS === "ios" && Platform.isPad && nativeControls?.supportsWorkspaceColumns === true; +// Binaries without the native no-hinge fallback never report metrics on ordinary +// iPhones running iOS 27.1 or later. Use the compact navigator rather than staying blank. +const NATIVE_METRICS_TIMEOUT_MS = 1000; + export function useNativeWorkspaceColumnsReady() { const metrics = useNativeLayoutMetrics(); + const [timedOut, setTimedOut] = useState(false); // Choose the phone's native host before attaching any screens. Older clients // without the observer can immediately use the compact navigator. - return ( - (Platform.OS === "ios" && Platform.isPad) || - !nativeControls?.ViewPrototypes?.T3NativeControls_LayoutMetrics || - metrics !== null - ); + const waiting = + !(Platform.OS === "ios" && Platform.isPad) && + !!nativeControls?.ViewPrototypes?.T3NativeControls_LayoutMetrics && + metrics === null; + + useEffect(() => { + if (!waiting) return; + const timeout = setTimeout(() => setTimedOut(true), NATIVE_METRICS_TIMEOUT_MS); + return () => clearTimeout(timeout); + }, [waiting]); + + return !waiting || timedOut; } export function useNativeWorkspaceColumnsSupported() {