Fence Duo APIs by UIKit SDK availability

Keep the standard navigation and sheet paths compilable with older SDKs. Exercise native fallback behavior and include the status editor in the native control harness.

Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-10-27 14:28:02 +00:00
parent 8e7a430b4a
commit 682ee55498
6 changed files with 42 additions and 5 deletions

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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 \