[url_launcher] migrating objc plugin to swift (#4753)

This PR converts the iOS portion of the url_launcher plugin from objc to swift. 

*List which issues are fixed by this PR. You must list at least one issue.*
https://github.com/flutter/flutter/issues/119102
diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md
index ae63012..4e9d077 100644
--- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 6.2.1
+
+* Migrates plugin from Objective-C to Swift.
+
 ## 6.2.0
 
 * Implements `supportsMode` and `supportsCloseForMode`.
diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj
index e40cd34..c10bff1 100644
--- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj
@@ -269,7 +269,7 @@
 		97C146E61CF9000F007C117D /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastUpgradeCheck = 1300;
+				LastUpgradeCheck = 1430;
 				ORGANIZATIONNAME = "The Flutter Authors";
 				TargetAttributes = {
 					97C146ED1CF9000F007C117D = {
@@ -631,6 +631,7 @@
 			baseConfigurationReference = 666BCD7C181C34F8BE58929B /* Pods-RunnerTests.debug.xcconfig */;
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_STYLE = Automatic;
 				INFOPLIST_FILE = RunnerTests/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
@@ -651,6 +652,7 @@
 			baseConfigurationReference = D25C434271ACF6555E002440 /* Pods-RunnerTests.release.xcconfig */;
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_STYLE = Automatic;
 				INFOPLIST_FILE = RunnerTests/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index ad0ebfa..fa4e0bb 100644
--- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1300"
+   LastUpgradeVersion = "1430"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift
index e11a7e3..966a3d6 100644
--- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift
+++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift
@@ -7,59 +7,52 @@
 
 @testable import url_launcher_ios
 
+// Tests whether NSURL parsing is strict. When linking against the iOS 17 SDK or later,
+// NSURL uses a more lenient parser which will not return nil.
+private func urlParsingIsStrict() -> Bool {
+  return URL(string: "b a d U R L") == nil
+}
+
 final class URLLauncherTests: XCTestCase {
 
-  private func createPlugin() -> FLTURLLauncherPlugin {
+  private func createPlugin() -> URLLauncherPlugin {
     let launcher = FakeLauncher()
-    return FLTURLLauncherPlugin(launcher: launcher)
+    return URLLauncherPlugin(launcher: launcher)
   }
 
-  private func createPlugin(launcher: FakeLauncher) -> FLTURLLauncherPlugin {
-    FLTURLLauncherPlugin(launcher: launcher)
+  private func createPlugin(launcher: FakeLauncher) -> URLLauncherPlugin {
+    return URLLauncherPlugin(launcher: launcher)
   }
 
   func testCanLaunchSuccess() {
-    var error: FlutterError?
-    let result = createPlugin().canLaunchURL("good://url", error: &error)
-
-    XCTAssertNotNil(result)
-    XCTAssertTrue(result?.boolValue ?? false)
-    XCTAssertNil(error)
+    let result = createPlugin().canLaunchUrl(url: "good://url")
+    XCTAssertEqual(result, .success)
   }
 
   func testCanLaunchFailure() {
-    var error: FlutterError?
-    let result = createPlugin().canLaunchURL("bad://url", error: &error)
-
-    XCTAssertNotNil(result)
-    XCTAssertFalse(result?.boolValue ?? true)
+    let result = createPlugin().canLaunchUrl(url: "bad://url")
+    XCTAssertEqual(result, .failure)
   }
 
   func testCanLaunchFailureWithInvalidURL() {
-    var error: FlutterError?
-    let result = createPlugin().canLaunchURL("urls can't have spaces", error: &error)
+    let result = createPlugin().canLaunchUrl(url: "urls can't have spaces")
 
-    if (error == nil) {
-      // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't
-      // fail to parse URLs, so the test must allow for either outcome.
-      XCTAssertNotNil(result)
-      XCTAssertFalse(result?.boolValue ?? true)
-      XCTAssertNil(error)
+    if urlParsingIsStrict() {
+      XCTAssertEqual(result, .invalidUrl)
     } else {
-      XCTAssertNil(result)
-      XCTAssertNotNil(error)
-      XCTAssertEqual(error?.code, "argument_error")
-      XCTAssertEqual(error?.message, "Unable to parse URL")
-      XCTAssertEqual(error?.details as? String, "Provided URL: urls can't have spaces")
+      XCTAssertEqual(result, .failure)
     }
   }
 
   func testLaunchSuccess() {
     let expectation = XCTestExpectation(description: "completion called")
-    createPlugin().launchURL("good://url", universalLinksOnly: false) { result, error in
-      XCTAssertNotNil(result)
-      XCTAssertTrue(result?.boolValue ?? false)
-      XCTAssertNil(error)
+    createPlugin().launchUrl(url: "good://url", universalLinksOnly: false) { result in
+      switch result {
+      case .success(let details):
+        XCTAssertEqual(details, .success)
+      case .failure(let error):
+        XCTFail("Unexpected error: \(error)")
+      }
       expectation.fulfill()
     }
 
@@ -68,11 +61,13 @@
 
   func testLaunchFailure() {
     let expectation = XCTestExpectation(description: "completion called")
-
-    createPlugin().launchURL("bad://url", universalLinksOnly: false) { result, error in
-      XCTAssertNotNil(result)
-      XCTAssertFalse(result?.boolValue ?? true)
-      XCTAssertNil(error)
+    createPlugin().launchUrl(url: "bad://url", universalLinksOnly: false) { result in
+      switch result {
+      case .success(let details):
+        XCTAssertEqual(details, .failure)
+      case .failure(let error):
+        XCTFail("Unexpected error: \(error)")
+      }
       expectation.fulfill()
     }
 
@@ -81,22 +76,17 @@
 
   func testLaunchFailureWithInvalidURL() {
     let expectation = XCTestExpectation(description: "completion called")
-
-    createPlugin().launchURL("urls can't have spaces", universalLinksOnly: false) { result, error in
-      if (error == nil) {
-        // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't
-        // fail to parse URLs, so the test must allow for either outcome.
-        XCTAssertNotNil(result)
-        XCTAssertFalse(result?.boolValue ?? true)
-        XCTAssertNil(error)
-      } else {
-        XCTAssertNil(result)
-        XCTAssertNotNil(error)
-        XCTAssertEqual(error?.code, "argument_error")
-        XCTAssertEqual(error?.message, "Unable to parse URL")
-        XCTAssertEqual(error?.details as? String, "Provided URL: urls can't have spaces")
+    createPlugin().launchUrl(url: "urls can't have spaces", universalLinksOnly: false) { result in
+      switch result {
+      case .success(let details):
+        if urlParsingIsStrict() {
+          XCTAssertEqual(details, .invalidUrl)
+        } else {
+          XCTAssertEqual(details, .failure)
+        }
+      case .failure(let error):
+        XCTFail("Unexpected error: \(error)")
       }
-
       expectation.fulfill()
     }
 
@@ -108,13 +98,17 @@
     let plugin = createPlugin(launcher: launcher)
 
     let expectation = XCTestExpectation(description: "completion called")
-    plugin.launchURL("good://url", universalLinksOnly: false) { result, error in
-      XCTAssertNil(error)
+    plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in
+      switch result {
+      case .success(let details):
+        XCTAssertEqual(details, .success)
+      case .failure(let error):
+        XCTFail("Unexpected error: \(error)")
+      }
       expectation.fulfill()
     }
 
     wait(for: [expectation], timeout: 1)
-
     XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, false)
   }
 
@@ -123,31 +117,35 @@
     let plugin = createPlugin(launcher: launcher)
 
     let expectation = XCTestExpectation(description: "completion called")
-
-    plugin.launchURL("good://url", universalLinksOnly: true) { result, error in
-      XCTAssertNil(error)
+    plugin.launchUrl(url: "good://url", universalLinksOnly: true) { result in
+      switch result {
+      case .success(let details):
+        XCTAssertEqual(details, .success)
+      case .failure(let error):
+        XCTFail("Unexpected error: \(error)")
+      }
       expectation.fulfill()
     }
 
     wait(for: [expectation], timeout: 1)
-
     XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, true)
   }
 
 }
 
-final private class FakeLauncher: NSObject, FULLauncher {
+final private class FakeLauncher: NSObject, Launcher {
   var passedOptions: [UIApplication.OpenExternalURLOptionsKey: Any]?
 
-  func canOpen(_ url: URL) -> Bool {
-    return url.scheme == "good"
+  func canOpenURL(_ url: URL) -> Bool {
+    url.scheme == "good"
   }
 
   func open(
-    _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any] = [:],
-    completionHandler: ((Bool) -> Void)? = nil
+    _ url: URL,
+    options: [UIApplication.OpenExternalURLOptionsKey: Any],
+    completionHandler completion: ((Bool) -> Void)?
   ) {
     self.passedOptions = options
-    completionHandler?(url.scheme == "good")
+    completion?(url.scheme == "good")
   }
 }
diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h
deleted file mode 100644
index 7b3480e..0000000
--- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#import <Flutter/Flutter.h>
-
-#import "messages.g.h"
-
-@interface FLTURLLauncherPlugin : NSObject <FlutterPlugin, FULUrlLauncherApi>
-@end
diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m
deleted file mode 100644
index 5d6a75f..0000000
--- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m
+++ /dev/null
@@ -1,202 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#import <SafariServices/SafariServices.h>
-
-#import "FLTURLLauncherPlugin.h"
-#import "FLTURLLauncherPlugin_Test.h"
-#import "FULLauncher.h"
-#import "messages.g.h"
-
-typedef void (^OpenInSafariVCResponse)(NSNumber *_Nullable, FlutterError *_Nullable);
-
-@interface FLTURLLaunchSession : NSObject <SFSafariViewControllerDelegate>
-
-@property(copy, nonatomic) OpenInSafariVCResponse completion;
-@property(strong, nonatomic) NSURL *url;
-@property(strong, nonatomic) SFSafariViewController *safari;
-@property(nonatomic, copy) void (^didFinish)(void);
-
-@end
-
-@implementation FLTURLLaunchSession
-
-- (instancetype)initWithURL:url completion:completion {
-  self = [super init];
-  if (self) {
-    self.url = url;
-    self.completion = completion;
-    self.safari = [[SFSafariViewController alloc] initWithURL:url];
-    self.safari.delegate = self;
-  }
-  return self;
-}
-
-- (void)safariViewController:(SFSafariViewController *)controller
-      didCompleteInitialLoad:(BOOL)didLoadSuccessfully {
-  if (didLoadSuccessfully) {
-    self.completion(@YES, nil);
-  } else {
-    self.completion(
-        nil, [FlutterError
-                 errorWithCode:@"Error"
-                       message:[NSString stringWithFormat:@"Error while launching %@", self.url]
-                       details:nil]);
-  }
-}
-
-- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller {
-  [controller dismissViewControllerAnimated:YES completion:nil];
-  self.didFinish();
-}
-
-- (void)close {
-  [self safariViewControllerDidFinish:self.safari];
-}
-
-@end
-
-#pragma mark -
-
-/// Default implementation of FULLancher, using UIApplication.
-@interface FULUIApplicationLauncher : NSObject <FULLauncher>
-@end
-
-@implementation FULUIApplicationLauncher
-- (BOOL)canOpenURL:(nonnull NSURL *)url {
-  return [[UIApplication sharedApplication] canOpenURL:url];
-}
-
-- (void)openURL:(nonnull NSURL *)url
-              options:(nonnull NSDictionary<UIApplicationOpenExternalURLOptionsKey, id> *)options
-    completionHandler:(void (^_Nullable)(BOOL))completion {
-  [[UIApplication sharedApplication] openURL:url options:options completionHandler:completion];
-}
-
-@end
-
-#pragma mark -
-
-@interface FLTURLLauncherPlugin ()
-
-@property(strong, nonatomic) FLTURLLaunchSession *currentSession;
-@property(strong, nonatomic) NSObject<FULLauncher> *launcher;
-
-@end
-
-@implementation FLTURLLauncherPlugin
-
-+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
-  FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] init];
-  FULUrlLauncherApiSetup(registrar.messenger, plugin);
-}
-
-- (instancetype)init {
-  return [self initWithLauncher:[[FULUIApplicationLauncher alloc] init]];
-}
-
-- (instancetype)initWithLauncher:(NSObject<FULLauncher> *)launcher {
-  if (self = [super init]) {
-    _launcher = launcher;
-  }
-  return self;
-}
-
-- (nullable NSNumber *)canLaunchURL:(NSString *)urlString
-                              error:(FlutterError *_Nullable *_Nonnull)error {
-  NSURL *url = [NSURL URLWithString:urlString];
-  if (!url) {
-    *error = [self invalidURLErrorForURLString:urlString];
-    return nil;
-  }
-  return @([self.launcher canOpenURL:url]);
-}
-
-- (void)launchURL:(NSString *)urlString
-    universalLinksOnly:(NSNumber *)universalLinksOnly
-            completion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion {
-  NSURL *url = [NSURL URLWithString:urlString];
-  if (!url) {
-    completion(nil, [self invalidURLErrorForURLString:urlString]);
-    return;
-  }
-  NSDictionary *options = @{UIApplicationOpenURLOptionUniversalLinksOnly : universalLinksOnly};
-  [self.launcher openURL:url
-                 options:options
-       completionHandler:^(BOOL success) {
-         completion(@(success), nil);
-       }];
-}
-
-- (void)openSafariViewControllerWithURL:(NSString *)urlString
-                             completion:(OpenInSafariVCResponse)completion {
-  NSURL *url = [NSURL URLWithString:urlString];
-  if (!url) {
-    completion(nil, [self invalidURLErrorForURLString:urlString]);
-    return;
-  }
-  self.currentSession = [[FLTURLLaunchSession alloc] initWithURL:url completion:completion];
-  __weak typeof(self) weakSelf = self;
-  self.currentSession.didFinish = ^(void) {
-    weakSelf.currentSession = nil;
-  };
-  [self.topViewController presentViewController:self.currentSession.safari
-                                       animated:YES
-                                     completion:nil];
-}
-
-- (void)closeSafariViewControllerWithError:(FlutterError *_Nullable *_Nonnull)error {
-  [self.currentSession close];
-}
-
-- (UIViewController *)topViewController {
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-  // TODO(stuartmorgan) Provide a non-deprecated codepath. See
-  // https://github.com/flutter/flutter/issues/104117
-  return [self topViewControllerFromViewController:[UIApplication sharedApplication]
-                                                       .keyWindow.rootViewController];
-#pragma clang diagnostic pop
-}
-
-/**
- * This method recursively iterate through the view hierarchy
- * to return the top most view controller.
- *
- * It supports the following scenarios:
- *
- * - The view controller is presenting another view.
- * - The view controller is a UINavigationController.
- * - The view controller is a UITabBarController.
- *
- * @return The top most view controller.
- */
-- (UIViewController *)topViewControllerFromViewController:(UIViewController *)viewController {
-  if ([viewController isKindOfClass:[UINavigationController class]]) {
-    UINavigationController *navigationController = (UINavigationController *)viewController;
-    return [self
-        topViewControllerFromViewController:[navigationController.viewControllers lastObject]];
-  }
-  if ([viewController isKindOfClass:[UITabBarController class]]) {
-    UITabBarController *tabController = (UITabBarController *)viewController;
-    return [self topViewControllerFromViewController:tabController.selectedViewController];
-  }
-  if (viewController.presentedViewController) {
-    return [self topViewControllerFromViewController:viewController.presentedViewController];
-  }
-  return viewController;
-}
-
-/**
- * Creates an error for an invalid URL string.
- *
- * @param url The invalid URL string
- * @return The error to return
- */
-- (FlutterError *)invalidURLErrorForURLString:(NSString *)url {
-  return [FlutterError errorWithCode:@"argument_error"
-                             message:@"Unable to parse URL"
-                             details:[NSString stringWithFormat:@"Provided URL: %@", url]];
-}
-@end
diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin_Test.h b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin_Test.h
deleted file mode 100644
index 112682a..0000000
--- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin_Test.h
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#import "FLTURLLauncherPlugin.h"
-#import "FULLauncher.h"
-
-/// APIs exposed for testing.
-@interface FLTURLLauncherPlugin (Test)
-- (instancetype)initWithLauncher:(NSObject<FULLauncher> *)launcher;
-@end
diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/FULLauncher.h b/packages/url_launcher/url_launcher_ios/ios/Classes/FULLauncher.h
deleted file mode 100644
index 63f8e04..0000000
--- a/packages/url_launcher/url_launcher_ios/ios/Classes/FULLauncher.h
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#import <Foundation/Foundation.h>
-
-NS_ASSUME_NONNULL_BEGIN
-
-/// Protocol for UIApplication methods relating to launching URLs.
-///
-/// This protocol exists to allow injecting an alternate implementation for testing.
-@protocol FULLauncher
-- (BOOL)canOpenURL:(NSURL *)url;
-- (void)openURL:(NSURL *)url
-              options:(NSDictionary<UIApplicationOpenExternalURLOptionsKey, id> *)options
-    completionHandler:(void (^__nullable)(BOOL success))completion;
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift
new file mode 100644
index 0000000..f97db9d
--- /dev/null
+++ b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift
@@ -0,0 +1,20 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Protocol for UIApplication methods relating to launching URLs.
+///
+/// This protocol exists to allow injecting an alternate implementation for testing.
+protocol Launcher {
+  /// Returns a Boolean value that indicates whether an app is available to handle a URL scheme.
+  func canOpenURL(_ url: URL) -> Bool
+
+  /// Attempts to asynchronously open the resource at the specified URL.
+  func open(
+    _ url: URL,
+    options: [UIApplication.OpenExternalURLOptionsKey: Any],
+    completionHandler completion: ((Bool) -> Void)?)
+}
+
+/// Launcher is intentionally a direct passthroguh to UIApplication.
+extension UIApplication: Launcher {}
diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift
new file mode 100644
index 0000000..b0761e5
--- /dev/null
+++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift
@@ -0,0 +1,63 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import Flutter
+import SafariServices
+
+typealias OpenInSafariCompletionHandler = (Result<InAppLoadResult, Error>) -> Void
+
+/// A session responsible for launching a URL in Safari and handling its events.
+final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate {
+
+  private let completion: OpenInSafariCompletionHandler
+  private let url: URL
+
+  /// The Safari view controller used for displaying the URL.
+  let safariViewController: SFSafariViewController
+
+  // A closure to be executed after the Safari view controller finishes.
+  var didFinish: (() -> Void)?
+
+  /// Initializes a new URLLaunchSession with the provided URL and completion handler.
+  ///
+  /// - Parameters:
+  ///   - url: The URL to be opened in Safari.
+  ///   - completion: The completion handler to be called after attempting to open the URL.
+  init(url: URL, completion: @escaping OpenInSafariCompletionHandler) {
+    self.url = url
+    self.completion = completion
+    self.safariViewController = SFSafariViewController(url: url)
+    super.init()
+    self.safariViewController.delegate = self
+  }
+
+  /// Called when the Safari view controller completes the initial load.
+  ///
+  /// - Parameters:
+  ///   - controller: The Safari view controller.
+  ///   - didLoadSuccessfully: Indicates if the initial load was successful.
+  func safariViewController(
+    _ controller: SFSafariViewController,
+    didCompleteInitialLoad didLoadSuccessfully: Bool
+  ) {
+    if didLoadSuccessfully {
+      completion(.success(.success))
+    } else {
+      completion(.success(.failedToLoad))
+    }
+  }
+
+  /// Called when the user finishes using the Safari view controller.
+  ///
+  /// - Parameter controller: The Safari view controller.
+  func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
+    controller.dismiss(animated: true, completion: nil)
+    didFinish?()
+  }
+
+  /// Closes the Safari view controller.
+  func close() {
+    safariViewControllerDidFinish(safariViewController)
+  }
+}
diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift
new file mode 100644
index 0000000..1880031
--- /dev/null
+++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift
@@ -0,0 +1,99 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import Flutter
+
+public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi {
+
+  public static func register(with registrar: FlutterPluginRegistrar) {
+    let plugin = URLLauncherPlugin()
+    UrlLauncherApiSetup.setUp(binaryMessenger: registrar.messenger(), api: plugin)
+    registrar.publish(plugin)
+  }
+
+  private var currentSession: URLLaunchSession?
+  private let launcher: Launcher
+
+  private var topViewController: UIViewController? {
+    // TODO(stuartmorgan) Provide a non-deprecated codepath. See
+    // https://github.com/flutter/flutter/issues/104117
+    UIApplication.shared.keyWindow?.rootViewController?.topViewController
+  }
+
+  init(launcher: Launcher = UIApplication.shared) {
+    self.launcher = launcher
+  }
+
+  func canLaunchUrl(url: String) -> LaunchResult {
+    guard let url = URL(string: url) else {
+      return .invalidUrl
+    }
+    let canOpen = launcher.canOpenURL(url)
+    return canOpen ? .success : .failure
+  }
+
+  func launchUrl(
+    url: String,
+    universalLinksOnly: Bool,
+    completion: @escaping (Result<LaunchResult, Error>) -> Void
+  ) {
+    guard let url = URL(string: url) else {
+      completion(.success(.invalidUrl))
+      return
+    }
+    let options = [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: universalLinksOnly]
+    launcher.open(url, options: options) { result in
+      completion(.success(result ? .success : .failure))
+    }
+  }
+
+  func openUrlInSafariViewController(
+    url: String,
+    completion: @escaping (Result<InAppLoadResult, Error>) -> Void
+  ) {
+    guard let url = URL(string: url) else {
+      completion(.success(.invalidUrl))
+      return
+    }
+
+    let session = URLLaunchSession(url: url, completion: completion)
+    currentSession = session
+
+    session.didFinish = { [weak self] in
+      self?.currentSession = nil
+    }
+    topViewController?.present(session.safariViewController, animated: true, completion: nil)
+  }
+
+  func closeSafariViewController() {
+    currentSession?.close()
+  }
+}
+
+/// This method recursively iterates through the view hierarchy
+/// to return the top-most view controller.
+///
+/// It supports the following scenarios:
+///
+/// - The view controller is presenting another view.
+/// - The view controller is a UINavigationController.
+/// - The view controller is a UITabBarController.
+///
+/// @return The top most view controller.
+extension UIViewController {
+  var topViewController: UIViewController {
+    if let navigationController = self as? UINavigationController {
+      return navigationController.viewControllers.last?.topViewController
+        ?? navigationController
+        .visibleViewController ?? navigationController
+    }
+    if let tabBarController = self as? UITabBarController {
+      return tabBarController.selectedViewController?.topViewController ?? tabBarController
+    }
+    if let presented = presentedViewController {
+      return presented.topViewController
+    }
+    return self
+  }
+}
diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.h b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.h
deleted file mode 100644
index 3a63e07..0000000
--- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.h
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-// Autogenerated from Pigeon (v9.2.4), do not edit directly.
-// See also: https://pub.dev/packages/pigeon
-
-#import <Foundation/Foundation.h>
-
-@protocol FlutterBinaryMessenger;
-@protocol FlutterMessageCodec;
-@class FlutterError;
-@class FlutterStandardTypedData;
-
-NS_ASSUME_NONNULL_BEGIN
-
-/// The codec used by FULUrlLauncherApi.
-NSObject<FlutterMessageCodec> *FULUrlLauncherApiGetCodec(void);
-
-@protocol FULUrlLauncherApi
-/// Returns true if the URL can definitely be launched.
-///
-/// @return `nil` only when `error != nil`.
-- (nullable NSNumber *)canLaunchURL:(NSString *)url error:(FlutterError *_Nullable *_Nonnull)error;
-/// Opens the URL externally, returning true if successful.
-- (void)launchURL:(NSString *)url
-    universalLinksOnly:(NSNumber *)universalLinksOnly
-            completion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion;
-/// Opens the URL in an in-app SFSafariViewController, returning true
-/// when it has loaded successfully.
-- (void)openSafariViewControllerWithURL:(NSString *)url
-                             completion:
-                                 (void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion;
-/// Closes the view controller opened by [openUrlInSafariViewController].
-- (void)closeSafariViewControllerWithError:(FlutterError *_Nullable *_Nonnull)error;
-@end
-
-extern void FULUrlLauncherApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
-                                   NSObject<FULUrlLauncherApi> *_Nullable api);
-
-NS_ASSUME_NONNULL_END
diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.m b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.m
deleted file mode 100644
index 4a38efb..0000000
--- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.m
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-// Autogenerated from Pigeon (v9.2.4), do not edit directly.
-// See also: https://pub.dev/packages/pigeon
-
-#import "messages.g.h"
-#import <Flutter/Flutter.h>
-
-#if !__has_feature(objc_arc)
-#error File requires ARC to be enabled.
-#endif
-
-static NSArray *wrapResult(id result, FlutterError *error) {
-  if (error) {
-    return @[
-      error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null]
-    ];
-  }
-  return @[ result ?: [NSNull null] ];
-}
-static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) {
-  id result = array[key];
-  return (result == [NSNull null]) ? nil : result;
-}
-
-NSObject<FlutterMessageCodec> *FULUrlLauncherApiGetCodec(void) {
-  static FlutterStandardMessageCodec *sSharedObject = nil;
-  sSharedObject = [FlutterStandardMessageCodec sharedInstance];
-  return sSharedObject;
-}
-
-void FULUrlLauncherApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
-                            NSObject<FULUrlLauncherApi> *api) {
-  /// Returns true if the URL can definitely be launched.
-  {
-    FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
-           initWithName:@"dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl"
-        binaryMessenger:binaryMessenger
-                  codec:FULUrlLauncherApiGetCodec()];
-    if (api) {
-      NSCAssert([api respondsToSelector:@selector(canLaunchURL:error:)],
-                @"FULUrlLauncherApi api (%@) doesn't respond to @selector(canLaunchURL:error:)",
-                api);
-      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        NSArray *args = message;
-        NSString *arg_url = GetNullableObjectAtIndex(args, 0);
-        FlutterError *error;
-        NSNumber *output = [api canLaunchURL:arg_url error:&error];
-        callback(wrapResult(output, error));
-      }];
-    } else {
-      [channel setMessageHandler:nil];
-    }
-  }
-  /// Opens the URL externally, returning true if successful.
-  {
-    FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
-           initWithName:@"dev.flutter.pigeon.UrlLauncherApi.launchUrl"
-        binaryMessenger:binaryMessenger
-                  codec:FULUrlLauncherApiGetCodec()];
-    if (api) {
-      NSCAssert([api respondsToSelector:@selector(launchURL:universalLinksOnly:completion:)],
-                @"FULUrlLauncherApi api (%@) doesn't respond to "
-                @"@selector(launchURL:universalLinksOnly:completion:)",
-                api);
-      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        NSArray *args = message;
-        NSString *arg_url = GetNullableObjectAtIndex(args, 0);
-        NSNumber *arg_universalLinksOnly = GetNullableObjectAtIndex(args, 1);
-        [api launchURL:arg_url
-            universalLinksOnly:arg_universalLinksOnly
-                    completion:^(NSNumber *_Nullable output, FlutterError *_Nullable error) {
-                      callback(wrapResult(output, error));
-                    }];
-      }];
-    } else {
-      [channel setMessageHandler:nil];
-    }
-  }
-  /// Opens the URL in an in-app SFSafariViewController, returning true
-  /// when it has loaded successfully.
-  {
-    FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
-           initWithName:@"dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController"
-        binaryMessenger:binaryMessenger
-                  codec:FULUrlLauncherApiGetCodec()];
-    if (api) {
-      NSCAssert([api respondsToSelector:@selector(openSafariViewControllerWithURL:completion:)],
-                @"FULUrlLauncherApi api (%@) doesn't respond to "
-                @"@selector(openSafariViewControllerWithURL:completion:)",
-                api);
-      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        NSArray *args = message;
-        NSString *arg_url = GetNullableObjectAtIndex(args, 0);
-        [api openSafariViewControllerWithURL:arg_url
-                                  completion:^(NSNumber *_Nullable output,
-                                               FlutterError *_Nullable error) {
-                                    callback(wrapResult(output, error));
-                                  }];
-      }];
-    } else {
-      [channel setMessageHandler:nil];
-    }
-  }
-  /// Closes the view controller opened by [openUrlInSafariViewController].
-  {
-    FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
-           initWithName:@"dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController"
-        binaryMessenger:binaryMessenger
-                  codec:FULUrlLauncherApiGetCodec()];
-    if (api) {
-      NSCAssert([api respondsToSelector:@selector(closeSafariViewControllerWithError:)],
-                @"FULUrlLauncherApi api (%@) doesn't respond to "
-                @"@selector(closeSafariViewControllerWithError:)",
-                api);
-      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
-        [api closeSafariViewControllerWithError:&error];
-        callback(wrapResult(nil, error));
-      }];
-    } else {
-      [channel setMessageHandler:nil];
-    }
-  }
-}
diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift
new file mode 100644
index 0000000..c3b0b8a
--- /dev/null
+++ b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift
@@ -0,0 +1,163 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// Autogenerated from Pigeon (v11.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+import Foundation
+
+#if os(iOS)
+  import Flutter
+#elseif os(macOS)
+  import FlutterMacOS
+#else
+  #error("Unsupported platform.")
+#endif
+
+private func isNullish(_ value: Any?) -> Bool {
+  return value is NSNull || value == nil
+}
+
+private func wrapResult(_ result: Any?) -> [Any?] {
+  return [result]
+}
+
+private func wrapError(_ error: Any) -> [Any?] {
+  if let flutterError = error as? FlutterError {
+    return [
+      flutterError.code,
+      flutterError.message,
+      flutterError.details,
+    ]
+  }
+  return [
+    "\(error)",
+    "\(type(of: error))",
+    "Stacktrace: \(Thread.callStackSymbols)",
+  ]
+}
+
+private func nilOrValue<T>(_ value: Any?) -> T? {
+  if value is NSNull { return nil }
+  return value as! T?
+}
+
+/// Possible outcomes of launching a URL.
+enum LaunchResult: Int {
+  /// The URL was successfully launched (or could be, for `canLaunchUrl`).
+  case success = 0
+  /// There was no handler available for the URL.
+  case failure = 1
+  /// The URL could not be launched because it is invalid.
+  case invalidUrl = 2
+}
+
+/// Possible outcomes of handling a URL within the application.
+enum InAppLoadResult: Int {
+  /// The URL was successfully loaded.
+  case success = 0
+  /// The URL did not load successfully.
+  case failedToLoad = 1
+  /// The URL could not be launched because it is invalid.
+  case invalidUrl = 2
+}
+
+/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
+protocol UrlLauncherApi {
+  /// Checks whether a URL can be loaded.
+  func canLaunchUrl(url: String) throws -> LaunchResult
+  /// Opens the URL externally, returning the status of launching it.
+  func launchUrl(
+    url: String, universalLinksOnly: Bool,
+    completion: @escaping (Result<LaunchResult, Error>) -> Void)
+  /// Opens the URL in an in-app SFSafariViewController, returning the results
+  /// of loading it.
+  func openUrlInSafariViewController(
+    url: String, completion: @escaping (Result<InAppLoadResult, Error>) -> Void)
+  /// Closes the view controller opened by [openUrlInSafariViewController].
+  func closeSafariViewController() throws
+}
+
+/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
+class UrlLauncherApiSetup {
+  /// The codec used by UrlLauncherApi.
+  /// Sets up an instance of `UrlLauncherApi` to handle messages through the `binaryMessenger`.
+  static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UrlLauncherApi?) {
+    /// Checks whether a URL can be loaded.
+    let canLaunchUrlChannel = FlutterBasicMessageChannel(
+      name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl",
+      binaryMessenger: binaryMessenger)
+    if let api = api {
+      canLaunchUrlChannel.setMessageHandler { message, reply in
+        let args = message as! [Any?]
+        let urlArg = args[0] as! String
+        do {
+          let result = try api.canLaunchUrl(url: urlArg)
+          reply(wrapResult(result.rawValue))
+        } catch {
+          reply(wrapError(error))
+        }
+      }
+    } else {
+      canLaunchUrlChannel.setMessageHandler(nil)
+    }
+    /// Opens the URL externally, returning the status of launching it.
+    let launchUrlChannel = FlutterBasicMessageChannel(
+      name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl",
+      binaryMessenger: binaryMessenger)
+    if let api = api {
+      launchUrlChannel.setMessageHandler { message, reply in
+        let args = message as! [Any?]
+        let urlArg = args[0] as! String
+        let universalLinksOnlyArg = args[1] as! Bool
+        api.launchUrl(url: urlArg, universalLinksOnly: universalLinksOnlyArg) { result in
+          switch result {
+          case .success(let res):
+            reply(wrapResult(res.rawValue))
+          case .failure(let error):
+            reply(wrapError(error))
+          }
+        }
+      }
+    } else {
+      launchUrlChannel.setMessageHandler(nil)
+    }
+    /// Opens the URL in an in-app SFSafariViewController, returning the results
+    /// of loading it.
+    let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel(
+      name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController",
+      binaryMessenger: binaryMessenger)
+    if let api = api {
+      openUrlInSafariViewControllerChannel.setMessageHandler { message, reply in
+        let args = message as! [Any?]
+        let urlArg = args[0] as! String
+        api.openUrlInSafariViewController(url: urlArg) { result in
+          switch result {
+          case .success(let res):
+            reply(wrapResult(res.rawValue))
+          case .failure(let error):
+            reply(wrapError(error))
+          }
+        }
+      }
+    } else {
+      openUrlInSafariViewControllerChannel.setMessageHandler(nil)
+    }
+    /// Closes the view controller opened by [openUrlInSafariViewController].
+    let closeSafariViewControllerChannel = FlutterBasicMessageChannel(
+      name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController",
+      binaryMessenger: binaryMessenger)
+    if let api = api {
+      closeSafariViewControllerChannel.setMessageHandler { _, reply in
+        do {
+          try api.closeSafariViewController()
+          reply(wrapResult(nil))
+        } catch {
+          reply(wrapError(error))
+        }
+      }
+    } else {
+      closeSafariViewControllerChannel.setMessageHandler(nil)
+    }
+  }
+}
diff --git a/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec
index 3dd3eb9..400ad73 100644
--- a/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec
+++ b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec
@@ -14,7 +14,7 @@
   s.source           = { :http => 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_ios' }
   s.documentation_url = 'https://pub.dev/packages/url_launcher'
   s.swift_version = '5.0'
-  s.source_files = 'Classes/**/*'
+  s.source_files = 'Classes/**/*.swift'
   s.xcconfig = {
       'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift',
       'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
diff --git a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart
index 562a408..a7e9a8c 100644
--- a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart
+++ b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart
@@ -1,7 +1,7 @@
 // Copyright 2013 The Flutter Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
-// Autogenerated from Pigeon (v9.2.4), do not edit directly.
+// Autogenerated from Pigeon (v11.0.1), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
 
@@ -11,6 +11,30 @@
 import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
 import 'package:flutter/services.dart';
 
+/// Possible outcomes of launching a URL.
+enum LaunchResult {
+  /// The URL was successfully launched (or could be, for `canLaunchUrl`).
+  success,
+
+  /// There was no handler available for the URL.
+  failure,
+
+  /// The URL could not be launched because it is invalid.
+  invalidUrl,
+}
+
+/// Possible outcomes of handling a URL within the application.
+enum InAppLoadResult {
+  /// The URL was successfully loaded.
+  success,
+
+  /// The URL did not load successfully.
+  failedToLoad,
+
+  /// The URL could not be launched because it is invalid.
+  invalidUrl,
+}
+
 class UrlLauncherApi {
   /// Constructor for [UrlLauncherApi].  The [binaryMessenger] named argument is
   /// available for dependency injection.  If it is left null, the default
@@ -21,67 +45,10 @@
 
   static const MessageCodec<Object?> codec = StandardMessageCodec();
 
-  /// Returns true if the URL can definitely be launched.
-  Future<bool> canLaunchUrl(String arg_url) async {
+  /// Checks whether a URL can be loaded.
+  Future<LaunchResult> canLaunchUrl(String arg_url) async {
     final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-        'dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl', codec,
-        binaryMessenger: _binaryMessenger);
-    final List<Object?>? replyList =
-        await channel.send(<Object?>[arg_url]) as List<Object?>?;
-    if (replyList == null) {
-      throw PlatformException(
-        code: 'channel-error',
-        message: 'Unable to establish connection on channel.',
-      );
-    } else if (replyList.length > 1) {
-      throw PlatformException(
-        code: replyList[0]! as String,
-        message: replyList[1] as String?,
-        details: replyList[2],
-      );
-    } else if (replyList[0] == null) {
-      throw PlatformException(
-        code: 'null-error',
-        message: 'Host platform returned null value for non-null return value.',
-      );
-    } else {
-      return (replyList[0] as bool?)!;
-    }
-  }
-
-  /// Opens the URL externally, returning true if successful.
-  Future<bool> launchUrl(String arg_url, bool arg_universalLinksOnly) async {
-    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-        'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec,
-        binaryMessenger: _binaryMessenger);
-    final List<Object?>? replyList = await channel
-        .send(<Object?>[arg_url, arg_universalLinksOnly]) as List<Object?>?;
-    if (replyList == null) {
-      throw PlatformException(
-        code: 'channel-error',
-        message: 'Unable to establish connection on channel.',
-      );
-    } else if (replyList.length > 1) {
-      throw PlatformException(
-        code: replyList[0]! as String,
-        message: replyList[1] as String?,
-        details: replyList[2],
-      );
-    } else if (replyList[0] == null) {
-      throw PlatformException(
-        code: 'null-error',
-        message: 'Host platform returned null value for non-null return value.',
-      );
-    } else {
-      return (replyList[0] as bool?)!;
-    }
-  }
-
-  /// Opens the URL in an in-app SFSafariViewController, returning true
-  /// when it has loaded successfully.
-  Future<bool> openUrlInSafariViewController(String arg_url) async {
-    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-        'dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController',
+        'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl',
         codec,
         binaryMessenger: _binaryMessenger);
     final List<Object?>? replyList =
@@ -103,14 +70,74 @@
         message: 'Host platform returned null value for non-null return value.',
       );
     } else {
-      return (replyList[0] as bool?)!;
+      return LaunchResult.values[replyList[0]! as int];
+    }
+  }
+
+  /// Opens the URL externally, returning the status of launching it.
+  Future<LaunchResult> launchUrl(
+      String arg_url, bool arg_universalLinksOnly) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList = await channel
+        .send(<Object?>[arg_url, arg_universalLinksOnly]) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else if (replyList[0] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return LaunchResult.values[replyList[0]! as int];
+    }
+  }
+
+  /// Opens the URL in an in-app SFSafariViewController, returning the results
+  /// of loading it.
+  Future<InAppLoadResult> openUrlInSafariViewController(String arg_url) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController',
+        codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList =
+        await channel.send(<Object?>[arg_url]) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else if (replyList[0] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return InAppLoadResult.values[replyList[0]! as int];
     }
   }
 
   /// Closes the view controller opened by [openUrlInSafariViewController].
   Future<void> closeSafariViewController() async {
     final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-        'dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController', codec,
+        'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController',
+        codec,
         binaryMessenger: _binaryMessenger);
     final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
     if (replyList == null) {
diff --git a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart
index 6696978..9d1ebc9 100644
--- a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart
+++ b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:flutter/foundation.dart' show visibleForTesting;
+import 'package:flutter/services.dart';
 import 'package:url_launcher_platform_interface/link.dart';
 import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
 
@@ -26,8 +27,9 @@
   final LinkDelegate? linkDelegate = null;
 
   @override
-  Future<bool> canLaunch(String url) {
-    return _hostApi.canLaunchUrl(url);
+  Future<bool> canLaunch(String url) async {
+    final LaunchResult result = await _hostApi.canLaunchUrl(url);
+    return _mapLaunchResult(result);
   }
 
   @override
@@ -90,10 +92,12 @@
     }
 
     if (inApp) {
-      return _hostApi.openUrlInSafariViewController(url);
+      return _mapInAppLoadResult(
+          await _hostApi.openUrlInSafariViewController(url),
+          url: url);
     } else {
-      return _hostApi.launchUrl(url,
-          options.mode == PreferredLaunchMode.externalNonBrowserApplication);
+      return _mapLaunchResult(await _hostApi.launchUrl(url,
+          options.mode == PreferredLaunchMode.externalNonBrowserApplication));
     }
   }
 
@@ -120,4 +124,52 @@
     return mode == PreferredLaunchMode.inAppWebView ||
         mode == PreferredLaunchMode.inAppBrowserView;
   }
+
+  bool _mapLaunchResult(LaunchResult result) {
+    switch (result) {
+      case LaunchResult.success:
+        return true;
+      case LaunchResult.failure:
+        return false;
+      case LaunchResult.invalidUrl:
+        throw _invalidUrlException();
+    }
+  }
+
+  bool _mapInAppLoadResult(InAppLoadResult result, {required String url}) {
+    switch (result) {
+      case InAppLoadResult.success:
+        return true;
+      case InAppLoadResult.failedToLoad:
+        throw _failedSafariViewControllerLoadException(url);
+      case InAppLoadResult.invalidUrl:
+        throw _invalidUrlException();
+    }
+  }
+
+  // TODO(stuartmorgan): Remove this as part of standardizing error handling.
+  // See https://github.com/flutter/flutter/issues/127665
+  //
+  // This PlatformException (including the exact string details, since those
+  // are a defacto part of the API) is for compatibility with the previous
+  // native implementation.
+  PlatformException _invalidUrlException() {
+    throw PlatformException(
+      code: 'argument_error',
+      message: 'Unable to parse URL',
+    );
+  }
+
+  // TODO(stuartmorgan): Remove this as part of standardizing error handling.
+  // See https://github.com/flutter/flutter/issues/127665
+  //
+  // This PlatformException (including the exact string details, since those
+  // are a defacto part of the API) is for compatibility with the previous
+  // native implementation.
+  PlatformException _failedSafariViewControllerLoadException(String url) {
+    throw PlatformException(
+      code: 'Error',
+      message: 'Error while launching $url',
+    );
+  }
 }
diff --git a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart
index f6935cb..f5dc105 100644
--- a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart
+++ b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart
@@ -6,27 +6,50 @@
 
 @ConfigurePigeon(PigeonOptions(
   dartOut: 'lib/src/messages.g.dart',
-  objcOptions: ObjcOptions(prefix: 'FUL'),
-  objcHeaderOut: 'ios/Classes/messages.g.h',
-  objcSourceOut: 'ios/Classes/messages.g.m',
+  swiftOut: 'ios/Classes/messages.g.swift',
   copyrightHeader: 'pigeons/copyright.txt',
 ))
+
+/// Possible outcomes of launching a URL.
+enum LaunchResult {
+  /// The URL was successfully launched (or could be, for `canLaunchUrl`).
+  success,
+
+  /// There was no handler available for the URL.
+  failure,
+
+  /// The URL could not be launched because it is invalid.
+  invalidUrl,
+}
+
+/// Possible outcomes of handling a URL within the application.
+enum InAppLoadResult {
+  /// The URL was successfully loaded.
+  success,
+
+  /// The URL did not load successfully.
+  failedToLoad,
+
+  /// The URL could not be launched because it is invalid.
+  invalidUrl,
+}
+
 @HostApi()
 abstract class UrlLauncherApi {
-  /// Returns true if the URL can definitely be launched.
+  /// Checks whether a URL can be loaded.
   @ObjCSelector('canLaunchURL:')
-  bool canLaunchUrl(String url);
+  LaunchResult canLaunchUrl(String url);
 
-  /// Opens the URL externally, returning true if successful.
+  /// Opens the URL externally, returning the status of launching it.
   @async
   @ObjCSelector('launchURL:universalLinksOnly:')
-  bool launchUrl(String url, bool universalLinksOnly);
+  LaunchResult launchUrl(String url, bool universalLinksOnly);
 
-  /// Opens the URL in an in-app SFSafariViewController, returning true
-  /// when it has loaded successfully.
+  /// Opens the URL in an in-app SFSafariViewController, returning the results
+  /// of loading it.
   @async
   @ObjCSelector('openSafariViewControllerWithURL:')
-  bool openUrlInSafariViewController(String url);
+  InAppLoadResult openUrlInSafariViewController(String url);
 
   /// Closes the view controller opened by [openUrlInSafariViewController].
   void closeSafariViewController();
diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml
index 6047568..56b337b 100644
--- a/packages/url_launcher/url_launcher_ios/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml
@@ -2,7 +2,7 @@
 description: iOS implementation of the url_launcher plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_ios
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
-version: 6.2.0
+version: 6.2.1
 
 environment:
   sdk: ">=2.19.0 <4.0.0"
@@ -13,7 +13,7 @@
     implements: url_launcher
     platforms:
       ios:
-        pluginClass: FLTURLLauncherPlugin
+        pluginClass: URLLauncherPlugin
         dartPluginClass: UrlLauncherIOS
 
 dependencies:
@@ -22,9 +22,11 @@
   url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
+  build_runner: ^2.3.3
   flutter_test:
     sdk: flutter
-  pigeon: ^9.2.4
+  mockito: 5.4.2
+  pigeon: ^11.0.1
   plugin_platform_interface: ^2.0.0
   test: ^1.16.3
 
diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart
index bacea31..195db63 100644
--- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart
+++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart
@@ -4,17 +4,23 @@
 
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
 import 'package:url_launcher_ios/src/messages.g.dart';
 import 'package:url_launcher_ios/url_launcher_ios.dart';
 import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
 
-void main() {
-  TestWidgetsFlutterBinding.ensureInitialized();
+import 'url_launcher_ios_test.mocks.dart';
 
-  late _FakeUrlLauncherApi api;
+// A web URL to use in tests where the specifics of the URL don't matter.
+const String _webUrl = 'https://example.com/';
+
+@GenerateMocks(<Type>[UrlLauncherApi])
+void main() {
+  late MockUrlLauncherApi api;
 
   setUp(() {
-    api = _FakeUrlLauncherApi();
+    api = MockUrlLauncherApi();
   });
 
   test('registers instance', () {
@@ -24,28 +30,38 @@
 
   group('canLaunch', () {
     test('handles success', () async {
+      when(api.canLaunchUrl(_webUrl))
+          .thenAnswer((_) async => LaunchResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
-      expect(await launcher.canLaunch('http://example.com/'), true);
+      expect(await launcher.canLaunch(_webUrl), true);
     });
 
     test('handles failure', () async {
+      when(api.canLaunchUrl(_webUrl))
+          .thenAnswer((_) async => LaunchResult.failure);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
-      expect(await launcher.canLaunch('unknown://scheme'), false);
+      expect(await launcher.canLaunch(_webUrl), false);
     });
 
-    test('passes invalid URL PlatformException through', () async {
+    test('throws PlatformException for invalid URL', () async {
+      when(api.canLaunchUrl(_webUrl))
+          .thenAnswer((_) async => LaunchResult.invalidUrl);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
-      await expectLater(launcher.canLaunch('invalid://u r l'),
-          throwsA(isA<PlatformException>()));
+      await expectLater(
+          launcher.canLaunch(_webUrl),
+          throwsA(isA<PlatformException>().having(
+              (PlatformException e) => e.code, 'code', 'argument_error')));
     });
   });
 
   group('legacy launch', () {
     test('handles success', () async {
+      when(api.launchUrl(_webUrl, any))
+          .thenAnswer((_) async => LaunchResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(
           await launcher.launch(
-            'http://example.com/',
+            _webUrl,
             useSafariVC: false,
             useWebView: false,
             enableJavaScript: false,
@@ -54,14 +70,16 @@
             headers: const <String, String>{},
           ),
           true);
-      expect(api.passedUniversalLinksOnly, false);
+      verifyNever(api.openUrlInSafariViewController(any));
     });
 
     test('handles failure', () async {
+      when(api.launchUrl(_webUrl, any))
+          .thenAnswer((_) async => LaunchResult.failure);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(
           await launcher.launch(
-            'unknown://scheme',
+            _webUrl,
             useSafariVC: false,
             useWebView: false,
             enableJavaScript: false,
@@ -70,14 +88,16 @@
             headers: const <String, String>{},
           ),
           false);
-      expect(api.passedUniversalLinksOnly, false);
+      verifyNever(api.openUrlInSafariViewController(any));
     });
 
-    test('passes invalid URL PlatformException through', () async {
+    test('throws PlatformException for invalid URL', () async {
+      when(api.launchUrl(_webUrl, any))
+          .thenAnswer((_) async => LaunchResult.invalidUrl);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       await expectLater(
           launcher.launch(
-            'invalid://u r l',
+            _webUrl,
             useSafariVC: false,
             useWebView: false,
             enableJavaScript: false,
@@ -85,14 +105,17 @@
             universalLinksOnly: false,
             headers: const <String, String>{},
           ),
-          throwsA(isA<PlatformException>()));
+          throwsA(isA<PlatformException>().having(
+              (PlatformException e) => e.code, 'code', 'argument_error')));
     });
 
     test('force SafariVC is handled', () async {
+      when(api.openUrlInSafariViewController(_webUrl))
+          .thenAnswer((_) async => InAppLoadResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(
           await launcher.launch(
-            'http://example.com/',
+            _webUrl,
             useSafariVC: true,
             useWebView: false,
             enableJavaScript: false,
@@ -101,14 +124,16 @@
             headers: const <String, String>{},
           ),
           true);
-      expect(api.usedSafariViewController, true);
+      verifyNever(api.launchUrl(any, any));
     });
 
     test('universal links only is handled', () async {
+      when(api.launchUrl(_webUrl, any))
+          .thenAnswer((_) async => LaunchResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(
           await launcher.launch(
-            'http://example.com/',
+            _webUrl,
             useSafariVC: false,
             useWebView: false,
             enableJavaScript: false,
@@ -117,14 +142,16 @@
             headers: const <String, String>{},
           ),
           true);
-      expect(api.passedUniversalLinksOnly, true);
+      verifyNever(api.openUrlInSafariViewController(any));
     });
 
     test('disallowing SafariVC is handled', () async {
+      when(api.launchUrl(_webUrl, any))
+          .thenAnswer((_) async => LaunchResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(
           await launcher.launch(
-            'http://example.com/',
+            _webUrl,
             useSafariVC: false,
             useWebView: false,
             enableJavaScript: false,
@@ -133,109 +160,147 @@
             headers: const <String, String>{},
           ),
           true);
-      expect(api.usedSafariViewController, false);
+      verifyNever(api.openUrlInSafariViewController(any));
     });
   });
 
   test('closeWebView calls through', () async {
     final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
     await launcher.closeWebView();
-    expect(api.closed, true);
+    verify(api.closeSafariViewController()).called(1);
   });
 
   group('launch without webview', () {
     test('calls through', () async {
+      when(api.launchUrl(_webUrl, any))
+          .thenAnswer((_) async => LaunchResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       final bool launched = await launcher.launchUrl(
-        'http://example.com/',
+        _webUrl,
         const LaunchOptions(mode: PreferredLaunchMode.externalApplication),
       );
       expect(launched, true);
-      expect(api.usedSafariViewController, false);
+      verifyNever(api.openUrlInSafariViewController(any));
     });
 
-    test('passes invalid URL PlatformException through', () async {
+    test('throws PlatformException for invalid URL', () async {
+      when(api.launchUrl(_webUrl, any))
+          .thenAnswer((_) async => LaunchResult.invalidUrl);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       await expectLater(
-          launcher.launchUrl('invalid://u r l', const LaunchOptions()),
-          throwsA(isA<PlatformException>()));
+          launcher.launchUrl(
+            _webUrl,
+            const LaunchOptions(mode: PreferredLaunchMode.externalApplication),
+          ),
+          throwsA(isA<PlatformException>().having(
+              (PlatformException e) => e.code, 'code', 'argument_error')));
     });
   });
 
   group('launch with Safari view controller', () {
     test('calls through with inAppWebView', () async {
+      when(api.openUrlInSafariViewController(_webUrl))
+          .thenAnswer((_) async => InAppLoadResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
-      final bool launched = await launcher.launchUrl('http://example.com/',
-          const LaunchOptions(mode: PreferredLaunchMode.inAppWebView));
+      final bool launched = await launcher.launchUrl(
+          _webUrl, const LaunchOptions(mode: PreferredLaunchMode.inAppWebView));
       expect(launched, true);
-      expect(api.usedSafariViewController, true);
+      verifyNever(api.launchUrl(any, any));
     });
 
     test('calls through with inAppBrowserView', () async {
+      when(api.openUrlInSafariViewController(_webUrl))
+          .thenAnswer((_) async => InAppLoadResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
-      final bool launched = await launcher.launchUrl('http://example.com/',
+      final bool launched = await launcher.launchUrl(_webUrl,
           const LaunchOptions(mode: PreferredLaunchMode.inAppBrowserView));
       expect(launched, true);
-      expect(api.usedSafariViewController, true);
+      verifyNever(api.launchUrl(any, any));
     });
 
-    test('passes invalid URL PlatformException through', () async {
+    test('throws PlatformException for invalid URL', () async {
+      when(api.openUrlInSafariViewController(_webUrl))
+          .thenAnswer((_) async => InAppLoadResult.invalidUrl);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       await expectLater(
-          launcher.launchUrl('invalid://u r l',
+          launcher.launchUrl(_webUrl,
               const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)),
-          throwsA(isA<PlatformException>()));
+          throwsA(isA<PlatformException>().having(
+              (PlatformException e) => e.code, 'code', 'argument_error')));
+    });
+
+    test('throws PlatformException for load failure', () async {
+      when(api.openUrlInSafariViewController(_webUrl))
+          .thenAnswer((_) async => InAppLoadResult.failedToLoad);
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      await expectLater(
+          launcher.launchUrl(_webUrl,
+              const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)),
+          throwsA(isA<PlatformException>()
+              .having((PlatformException e) => e.code, 'code', 'Error')));
     });
   });
 
   group('launch with universal links', () {
     test('calls through', () async {
+      when(api.launchUrl(_webUrl, any))
+          .thenAnswer((_) async => LaunchResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       final bool launched = await launcher.launchUrl(
-        'http://example.com/',
+        _webUrl,
         const LaunchOptions(
             mode: PreferredLaunchMode.externalNonBrowserApplication),
       );
       expect(launched, true);
-      expect(api.usedSafariViewController, false);
-      expect(api.passedUniversalLinksOnly, true);
+      verifyNever(api.openUrlInSafariViewController(any));
     });
 
-    test('passes invalid URL PlatformException through', () async {
+    test('throws PlatformException for invalid URL', () async {
+      when(api.launchUrl(_webUrl, any))
+          .thenAnswer((_) async => LaunchResult.invalidUrl);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       await expectLater(
           launcher.launchUrl(
-              'invalid://u r l',
+              _webUrl,
               const LaunchOptions(
                   mode: PreferredLaunchMode.externalNonBrowserApplication)),
-          throwsA(isA<PlatformException>()));
+          throwsA(isA<PlatformException>().having(
+              (PlatformException e) => e.code, 'code', 'argument_error')));
     });
   });
 
   group('launch with platform default', () {
     test('uses Safari view controller for http', () async {
+      const String httpUrl = 'http://example.com/';
+      when(api.openUrlInSafariViewController(httpUrl))
+          .thenAnswer((_) async => InAppLoadResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
-      final bool launched = await launcher.launchUrl(
-          'http://example.com/', const LaunchOptions());
+      final bool launched =
+          await launcher.launchUrl(httpUrl, const LaunchOptions());
       expect(launched, true);
-      expect(api.usedSafariViewController, true);
+      verifyNever(api.launchUrl(any, any));
     });
 
     test('uses Safari view controller for https', () async {
+      const String httpsUrl = 'https://example.com/';
+      when(api.openUrlInSafariViewController(httpsUrl))
+          .thenAnswer((_) async => InAppLoadResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
-      final bool launched = await launcher.launchUrl(
-          'https://example.com/', const LaunchOptions());
+      final bool launched =
+          await launcher.launchUrl(httpsUrl, const LaunchOptions());
       expect(launched, true);
-      expect(api.usedSafariViewController, true);
+      verifyNever(api.launchUrl(any, any));
     });
 
     test('uses standard external for other schemes', () async {
+      const String nonWebUrl = 'supportedcustomscheme://example.com/';
+      when(api.launchUrl(nonWebUrl, any))
+          .thenAnswer((_) async => LaunchResult.success);
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
-      final bool launched = await launcher.launchUrl(
-          'supportedcustomscheme://example.com/', const LaunchOptions());
+      final bool launched =
+          await launcher.launchUrl(nonWebUrl, const LaunchOptions());
       expect(launched, true);
-      expect(api.usedSafariViewController, false);
-      expect(api.passedUniversalLinksOnly, false);
+      verifyNever(api.openUrlInSafariViewController(any));
     });
   });
 
@@ -303,49 +368,3 @@
     });
   });
 }
-
-/// A fake implementation of the host API that reacts to specific schemes.
-///
-/// See _isLaunchable for the behaviors.
-class _FakeUrlLauncherApi implements UrlLauncherApi {
-  bool? passedUniversalLinksOnly;
-  bool? usedSafariViewController;
-  bool? closed;
-
-  @override
-  Future<bool> canLaunchUrl(String url) async {
-    return _isLaunchable(url);
-  }
-
-  @override
-  Future<bool> launchUrl(String url, bool universalLinksOnly) async {
-    passedUniversalLinksOnly = universalLinksOnly;
-    usedSafariViewController = false;
-    return _isLaunchable(url);
-  }
-
-  @override
-  Future<bool> openUrlInSafariViewController(String url) async {
-    usedSafariViewController = true;
-    return _isLaunchable(url);
-  }
-
-  @override
-  Future<void> closeSafariViewController() async {
-    closed = true;
-  }
-
-  bool _isLaunchable(String url) {
-    final String scheme = url.split(':')[0];
-    switch (scheme) {
-      case 'http':
-      case 'https':
-      case 'supportedcustomscheme':
-        return true;
-      case 'invalid':
-        throw PlatformException(code: 'argument_error');
-      default:
-        return false;
-    }
-  }
-}
diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart
new file mode 100644
index 0000000..e9eccab
--- /dev/null
+++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart
@@ -0,0 +1,79 @@
+// Mocks generated by Mockito 5.4.2 from annotations
+// in url_launcher_ios/test/url_launcher_ios_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i3;
+
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:url_launcher_ios/src/messages.g.dart' as _i2;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+/// A class which mocks [UrlLauncherApi].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockUrlLauncherApi extends _i1.Mock implements _i2.UrlLauncherApi {
+  MockUrlLauncherApi() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i3.Future<_i2.LaunchResult> canLaunchUrl(String? arg_url) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #canLaunchUrl,
+          [arg_url],
+        ),
+        returnValue:
+            _i3.Future<_i2.LaunchResult>.value(_i2.LaunchResult.success),
+      ) as _i3.Future<_i2.LaunchResult>);
+
+  @override
+  _i3.Future<_i2.LaunchResult> launchUrl(
+    String? arg_url,
+    bool? arg_universalLinksOnly,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #launchUrl,
+          [
+            arg_url,
+            arg_universalLinksOnly,
+          ],
+        ),
+        returnValue:
+            _i3.Future<_i2.LaunchResult>.value(_i2.LaunchResult.success),
+      ) as _i3.Future<_i2.LaunchResult>);
+
+  @override
+  _i3.Future<_i2.InAppLoadResult> openUrlInSafariViewController(
+          String? arg_url) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #openUrlInSafariViewController,
+          [arg_url],
+        ),
+        returnValue:
+            _i3.Future<_i2.InAppLoadResult>.value(_i2.InAppLoadResult.success),
+      ) as _i3.Future<_i2.InAppLoadResult>);
+
+  @override
+  _i3.Future<void> closeSafariViewController() => (super.noSuchMethod(
+        Invocation.method(
+          #closeSafariViewController,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+}