diff --git a/mobile/ios/Runner/IosNavigationBar.swift b/mobile/ios/Runner/IosNavigationBar.swift index 7a036785f0..7d7dc8294a 100644 --- a/mobile/ios/Runner/IosNavigationBar.swift +++ b/mobile/ios/Runner/IosNavigationBar.swift @@ -1,6 +1,9 @@ import Flutter import UIKit +// UIKit 9127.0.85 is the iOS 27.1 SDK with Duo APIs. SDK checks must +// surround runtime availability checks: older SDKs cannot resolve these symbols. + /// Each Flutter route owns a UIKit navigation controller. Its private scroll /// view mirrors the Flutter viewport so UIKit performs the large-title layout. final class IosNavigationBarFactory: NSObject, FlutterPlatformViewFactory { @@ -316,6 +319,7 @@ private final class IosNavigationBarView: NSObject, FlutterPlatformView { result(nil) } configure(args as? [String: Any] ?? [:]) + #if canImport(UIKit, _version: 9127.0.85) if #available(iOS 27.1, *), standaloneControl { container.addInteraction(UIHingeInteraction { [weak self] _, _ in guard let self else { return } @@ -327,6 +331,7 @@ private final class IosNavigationBarView: NSObject, FlutterPlatformView { DispatchQueue.main.asyncAfter(deadline: .now() + 1.2, execute: refresh) }) } + #endif } func view() -> UIView { container } @@ -421,6 +426,7 @@ private final class IosNavigationBarView: NSObject, FlutterPlatformView { } private func reportRailGeometry() { + #if canImport(UIKit, _version: 9127.0.85) guard #available(iOS 27.1, *), let window = container.window else { return } let inset = window.safeAreaInsets let right = inset.right > 0 && inset.left == 0 @@ -458,6 +464,7 @@ private final class IosNavigationBarView: NSObject, FlutterPlatformView { DispatchQueue.main.async { [weak self] in self?.channel.invokeMethod("railGeometry", arguments: values) } + #endif } private func reportAvatarBounds() { @@ -872,9 +879,11 @@ private final class IosNavigationBarView: NSObject, FlutterPlatformView { stack.bottomAnchor.constraint(equalTo: surface.contentView.bottomAnchor) ]) let item = UIBarButtonItem(customView: surface) + #if canImport(UIKit, _version: 9127.0.85) if #available(iOS 27.1, *), actions.allSatisfy({ $0["keepInHeaderOnDuo"] as? Bool == true }) { item.axisBehavior = .horizontalOnly } + #endif if #available(iOS 26.0, *) { item.hidesSharedBackground = true } return item } diff --git a/mobile/ios/Runner/NativeProfileTextEditor.swift b/mobile/ios/Runner/NativeProfileTextEditor.swift index 4a8fd38da2..419118a033 100644 --- a/mobile/ios/Runner/NativeProfileTextEditor.swift +++ b/mobile/ios/Runner/NativeProfileTextEditor.swift @@ -1,6 +1,9 @@ import Flutter import UIKit +// UIKit 9127.0.85 is the iOS 27.1 SDK with Duo APIs. SDK checks must +// surround runtime availability checks: older SDKs cannot resolve these symbols. + final class NativeProfileTextEditorCoordinator: NSObject, UIAdaptivePresentationControllerDelegate { @@ -207,11 +210,13 @@ final class NativeProfileTextEditorCoordinator: NSObject, @MainActor private func configureDuoPlacement(_ navigation: UINavigationController, presenter: UIViewController) { + #if canImport(UIKit, _version: 9127.0.85) guard #available(iOS 27.1, *) else { return } let insets = presenter.view.safeAreaInsets guard insets.top == 0, insets.left > 0 || insets.right > 0 else { return } navigation.sheetPresentationController?.preferredPlacement = presenter.view.effectiveUserInterfaceLayoutDirection == .rightToLeft ? .leading : .trailing + #endif } @MainActor @@ -432,10 +437,12 @@ final class NativeProfileTextEditorViewController: navigationItem.rightBarButtonItem?.image = UIImage(systemName: "checkmark") navigationItem.rightBarButtonItem?.accessibilityLabel = "Create" } + #if canImport(UIKit, _version: 9127.0.85) if #available(iOS 27.1, *) { navigationItem.leftBarButtonItem?.axisBehavior = .verticalPreferred navigationItem.rightBarButtonItem?.axisBehavior = .verticalPreferred } + #endif updateNavigation() } diff --git a/mobile/ios/Runner/NativeStatusEditor.swift b/mobile/ios/Runner/NativeStatusEditor.swift index f1d1a71cf5..64b5793b83 100644 --- a/mobile/ios/Runner/NativeStatusEditor.swift +++ b/mobile/ios/Runner/NativeStatusEditor.swift @@ -1,5 +1,8 @@ import UIKit +// UIKit 9127.0.85 is the iOS 27.1 SDK with Duo APIs. SDK checks must +// surround runtime availability checks: older SDKs cannot resolve these symbols. + /// Status uses the same navigation-controller sheet as the profile text forms. final class NativeStatusEditorViewController: UITableViewController, UITextFieldDelegate { let statusField = UITextField() @@ -48,10 +51,12 @@ final class NativeStatusEditorViewController: UITableViewController, UITextField navigationItem.leftBarButtonItem?.accessibilityLabel = "Close" navigationItem.rightBarButtonItem = UIBarButtonItem( title: "Set", style: .done, target: self, action: #selector(save)) + #if canImport(UIKit, _version: 9127.0.85) if #available(iOS 27.1, *) { navigationItem.leftBarButtonItem?.axisBehavior = .verticalPreferred navigationItem.rightBarButtonItem?.axisBehavior = .verticalPreferred } + #endif statusField.text = configuration["text"] as? String ?? "" statusField.placeholder = "What’s your status?" statusField.accessibilityLabel = "Status" diff --git a/mobile/ios/RunnerTests/ProfileSetupRecoveryTests.swift b/mobile/ios/RunnerTests/ProfileSetupRecoveryTests.swift index 4e20ee397b..805f3aca81 100644 --- a/mobile/ios/RunnerTests/ProfileSetupRecoveryTests.swift +++ b/mobile/ios/RunnerTests/ProfileSetupRecoveryTests.swift @@ -7,7 +7,6 @@ import XCTest final class ProfileSetupRecoveryTests: XCTestCase { @MainActor func testNativeEditorsPreferRightPaneOnlyWithDuoRail() async throws { - guard #available(iOS 27.1, *) else { return } for duo in [false, true] { for method in ["presentStatus", "present"] { let messenger = ProfileSetupTestMessenger() @@ -26,7 +25,12 @@ final class ProfileSetupRecoveryTests: XCTestCase { DispatchQueue.main.async { continuation.resume() } } let sheet = try XCTUnwrap(parent.captured?.sheetPresentationController) - XCTAssertEqual(sheet.preferredPlacement, duo ? .trailing : .automatic) + #if canImport(UIKit, _version: 9127.0.85) + if #available(iOS 27.1, *) { + XCTAssertEqual(sheet.preferredPlacement, duo ? .trailing : .automatic) + } + #endif + XCTAssertEqual(sheet.detents.map(\.identifier), [.large]) XCTAssertNil(sheet.sourceView) } } @@ -56,10 +60,12 @@ final class ProfileSetupRecoveryTests: XCTestCase { let submit = try XCTUnwrap(editor.navigationItem.rightBarButtonItem) XCTAssertEqual(cancel.accessibilityLabel, "Close") XCTAssertFalse(submit.isEnabled) + #if canImport(UIKit, _version: 9127.0.85) if #available(iOS 27.1, *) { XCTAssertEqual(cancel.axisBehavior, .verticalPreferred) XCTAssertEqual(submit.axisBehavior, .verticalPreferred) } + #endif XCTAssertEqual(editor.tableView(editor.tableView, numberOfRowsInSection: 1), 2) editor.tableView(editor.tableView, didSelectRowAt: IndexPath(row: 0, section: 2)) XCTAssertTrue(submit.isEnabled) @@ -94,10 +100,12 @@ final class ProfileSetupRecoveryTests: XCTestCase { editor.loadViewIfNeeded() let cancel = try XCTUnwrap(editor.navigationItem.leftBarButtonItem) let submit = try XCTUnwrap(editor.navigationItem.rightBarButtonItem) + #if canImport(UIKit, _version: 9127.0.85) if #available(iOS 27.1, *) { XCTAssertEqual(cancel.axisBehavior, .verticalPreferred) XCTAssertEqual(submit.axisBehavior, .verticalPreferred) } + #endif XCTAssertTrue(cancel.isEnabled) XCTAssertFalse(submit.isEnabled) UIApplication.shared.sendAction(try XCTUnwrap(cancel.action), to: cancel.target, from: cancel, for: nil) diff --git a/mobile/ios/RunnerTests/RunnerTests.swift b/mobile/ios/RunnerTests/RunnerTests.swift index aafb7073a0..0f314ea8c2 100644 --- a/mobile/ios/RunnerTests/RunnerTests.swift +++ b/mobile/ios/RunnerTests/RunnerTests.swift @@ -659,7 +659,6 @@ class RunnerTests: XCTestCase { @MainActor func testStandaloneRailObservesBookPosture() throws { - guard #available(iOS 27.1, *) else { return } for standalone in [false, true] { let parent = UIViewController() let bar = IosNavigationBarFactory( @@ -667,8 +666,16 @@ class RunnerTests: XCTestCase { ).create(withFrame: CGRect(x: 0, y: 0, width: 48, height: 48), viewIdentifier: 999989, arguments: ["standaloneControl": standalone]) - XCTAssertEqual( - bar.view().interactions.contains { $0 is UIHingeInteraction }, standalone) + #if canImport(UIKit, _version: 9127.0.85) + if #available(iOS 27.1, *) { + XCTAssertEqual( + bar.view().interactions.contains { $0 is UIHingeInteraction }, standalone) + } else { + XCTAssertTrue(bar.view().interactions.isEmpty) + } + #else + XCTAssertTrue(bar.view().interactions.isEmpty) + #endif } } diff --git a/scripts/test-mobile-me-agent-accessibility.sh b/scripts/test-mobile-me-agent-accessibility.sh index 6fd75eb013..49dfa0ec22 100755 --- a/scripts/test-mobile-me-agent-accessibility.sh +++ b/scripts/test-mobile-me-agent-accessibility.sh @@ -9,6 +9,7 @@ fixture=$(mktemp -d) trap 'rm -r "$fixture"' EXIT # Compile in one file to exercise the private production controller directly. cat "$repo_root/mobile/ios/Runner/MeAvatarLoader.swift" \ + "$repo_root/mobile/ios/Runner/NativeStatusEditor.swift" \ "$repo_root/mobile/ios/Runner/NativeProfileTextEditor.swift" \ "$repo_root/mobile/ios/NativeControlTests/MeAgentAccessibilityTests.swift" > "$fixture/test.swift" xcrun --sdk iphonesimulator swiftc -parse-as-library \