From 5e7542dcbd96c985c30b892ee50c33380c6fd18a Mon Sep 17 00:00:00 2001 From: kaluli123123 Date: Sun, 4 Oct 2026 19:22:06 +0800 Subject: [PATCH] fix(share_plus): preserve the subject when sharing a URI on iOS Forward the existing share title or legacy subject into the URI share controller. Keep raw NSURL items, completion behavior, and iPad presentation unchanged. Cover subject, title precedence, and no subject through the native registered channel. Closes #3994 Co-authored-by: Codex --- .../example/ios/RunnerTests/RunnerTests.swift | 79 ++++++++++++++++++- .../Sources/share_plus/FPPSharePlusPlugin.m | 4 +- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/packages/share_plus/share_plus/example/ios/RunnerTests/RunnerTests.swift b/packages/share_plus/share_plus/example/ios/RunnerTests/RunnerTests.swift index 86a7c3b1b6..107c13d6f3 100644 --- a/packages/share_plus/share_plus/example/ios/RunnerTests/RunnerTests.swift +++ b/packages/share_plus/share_plus/example/ios/RunnerTests/RunnerTests.swift @@ -1,12 +1,85 @@ import Flutter import UIKit import XCTest +import share_plus class RunnerTests: XCTestCase { - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + @MainActor + func testUriSharePreservesSubject() throws { + let controller = try shareUri(subject: "Subject for Mail") + XCTAssertEqual(controller.value(forKey: "subject") as? String, "Subject for Mail") } + @MainActor + func testUriShareUsesTitleBeforeLegacySubject() throws { + let controller = try shareUri(subject: "Legacy subject", title: "Share title") + XCTAssertEqual(controller.value(forKey: "subject") as? String, "Share title") + } + + @MainActor + func testUriShareWithoutSubject() throws { + let controller = try shareUri() + XCTAssertNil(controller.value(forKey: "subject")) + } + + @MainActor + private func shareUri(subject: String? = nil, title: String? = nil) throws + -> UIActivityViewController + { + let scene = try XCTUnwrap(UIApplication.shared.connectedScenes.first as? UIWindowScene) + let window = try XCTUnwrap(scene.windows.first(where: { $0.isKeyWindow })) + let previousController = window.rootViewController + let presenter = RecordingViewController() + window.rootViewController = presenter + defer { window.rootViewController = previousController } + + let engine = RecordingEngine(name: "share-plus-subject-test") + let registrar = try XCTUnwrap(engine.registrar(forPlugin: "share-plus-subject-test")) + FPPSharePlusPlugin.register(with: registrar) + let handler = try XCTUnwrap(engine.recorder.handler) + var arguments: [String: Any] = [ + "uri": "https://example.test/link", + "originX": 1.0, "originY": 1.0, "originWidth": 20.0, "originHeight": 20.0, + ] + if let subject { arguments["subject"] = subject } + if let title { arguments["title"] = title } + let call = FlutterMethodCall(methodName: "share", arguments: arguments) + handler(FlutterStandardMethodCodec.sharedInstance().encode(call)) { _ in + XCTFail("Sharing should wait for the activity to complete") + } + return try XCTUnwrap(presenter.activityController) + } +} + +private final class RecordingEngine: FlutterEngine { + let recorder = RecordingMessenger() + + override var binaryMessenger: FlutterBinaryMessenger { recorder } +} + +private final class RecordingMessenger: NSObject, FlutterBinaryMessenger { + var handler: FlutterBinaryMessageHandler? + + func send(onChannel channel: String, message: Data?) {} + func send(onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply?) {} + func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) {} + + func setMessageHandlerOnChannel( + _ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? + ) -> FlutterBinaryMessengerConnection { + if channel == "dev.fluttercommunity.plus/share" { self.handler = handler } + return 1 + } +} + +private final class RecordingViewController: UIViewController { + var activityController: UIActivityViewController? + + override func present( + _ viewControllerToPresent: UIViewController, animated flag: Bool, + completion: (() -> Void)? = nil + ) { + activityController = viewControllerToPresent as? UIActivityViewController + } } diff --git a/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m index 42a05468b2..66c7c24621 100644 --- a/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m +++ b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m @@ -363,6 +363,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { if (uri) { [self shareUri:uri + subject:shareTitle excludedActivityTypes:excludedActivityTypes withController:topViewController atSource:originRect @@ -458,13 +459,14 @@ + (void)share:(NSArray *)shareItems } + (void)shareUri:(NSString *)uri + subject:(NSString *)subject excludedActivityTypes:(NSArray *)excludedActivityTypes withController:(UIViewController *)controller atSource:(CGRect)origin toResult:(FlutterResult)result { NSURL *data = [NSURL URLWithString:uri]; [self share:@[ data ] - withSubject:nil + withSubject:subject excludedActivityTypes:excludedActivityTypes withController:controller atSource:origin