diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md index 86546d45566d..bfeeedcddfb0 100644 --- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.1 + +* Migrates iOS plugin to Swift. + ## 6.1.0 * Updates minimum Flutter version to 3.3 and iOS 11. 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 d61abc724469..23239294ae28 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 @@ -10,14 +10,14 @@ 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */; }; 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 856D0913184F79C678A42603 /* libPods-Runner.a */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 412154C429928BD30070F8F7 /* URLLauncherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412154C329928BD30070F8F7 /* URLLauncherTests.swift */; }; + 412154C629928CE50070F8F7 /* URLLauncherUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412154C529928CE50070F8F7 /* URLLauncherUITests.swift */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; B8140773523F70A044426500 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 487A1B5A2ECB3E406FD62FE3 /* libPods-RunnerTests.a */; }; - F7151F4B26604CFB0028CB91 /* URLLauncherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */; }; - F7151F5926604D060028CB91 /* URLLauncherUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F5826604D060028CB91 /* URLLauncherUITests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,6 +54,8 @@ 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GeneratedPluginRegistrant.h; path = Runner/GeneratedPluginRegistrant.h; sourceTree = ""; }; 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GeneratedPluginRegistrant.m; path = Runner/GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 412154C329928BD30070F8F7 /* URLLauncherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLLauncherTests.swift; sourceTree = ""; }; + 412154C529928CE50070F8F7 /* URLLauncherUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLLauncherUITests.swift; sourceTree = ""; }; 487A1B5A2ECB3E406FD62FE3 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 666BCD7C181C34F8BE58929B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -72,10 +74,8 @@ A84BFEE343F54B983D1B67EB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; D25C434271ACF6555E002440 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; F7151F4826604CFB0028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URLLauncherTests.m; sourceTree = ""; }; F7151F4C26604CFB0028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F7151F5626604D060028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F7151F5826604D060028CB91 /* URLLauncherUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URLLauncherUITests.m; sourceTree = ""; }; F7151F5A26604D060028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -187,8 +187,8 @@ F7151F4926604CFB0028CB91 /* RunnerTests */ = { isa = PBXGroup; children = ( - F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */, F7151F4C26604CFB0028CB91 /* Info.plist */, + 412154C329928BD30070F8F7 /* URLLauncherTests.swift */, ); path = RunnerTests; sourceTree = ""; @@ -196,8 +196,8 @@ F7151F5726604D060028CB91 /* RunnerUITests */ = { isa = PBXGroup; children = ( - F7151F5826604D060028CB91 /* URLLauncherUITests.m */, F7151F5A26604D060028CB91 /* Info.plist */, + 412154C529928CE50070F8F7 /* URLLauncherUITests.swift */, ); path = RunnerUITests; sourceTree = ""; @@ -278,11 +278,13 @@ }; F7151F4726604CFB0028CB91 = { CreatedOnToolsVersion = 12.5; + LastSwiftMigration = 1420; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; F7151F5526604D060028CB91 = { CreatedOnToolsVersion = 12.5; + LastSwiftMigration = 1420; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -425,7 +427,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F7151F4B26604CFB0028CB91 /* URLLauncherTests.m in Sources */, + 412154C429928BD30070F8F7 /* URLLauncherTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -433,7 +435,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F7151F5926604D060028CB91 /* URLLauncherUITests.m in Sources */, + 412154C629928CE50070F8F7 /* URLLauncherUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -589,7 +591,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -610,7 +615,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -625,12 +633,19 @@ 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 = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Debug; @@ -640,12 +655,18 @@ 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 = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Release; @@ -653,12 +674,19 @@ F7151F5E26604D060028CB91 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TEST_TARGET_NAME = Runner; }; name = Debug; @@ -666,12 +694,18 @@ F7151F5F26604D060028CB91 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_TARGET_NAME = Runner; }; name = Release; diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m deleted file mode 100644 index 6507a95a9d07..000000000000 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m +++ /dev/null @@ -1,18 +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 url_launcher_ios; -@import XCTest; - -@interface URLLauncherTests : XCTestCase -@end - -@implementation URLLauncherTests - -- (void)testPlugin { - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] init]; - XCTAssertNotNil(plugin); -} - -@end 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 new file mode 100644 index 000000000000..bc49f9c08592 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -0,0 +1,132 @@ +// 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 +import XCTest +import Flutter + +@testable import url_launcher_ios + +class URLLauncherTests: XCTestCase { + override func setUp() { + self.continueAfterFailure = false + } + + func testCreatePlugin() { + let plugin = URLLauncherPlugin() + XCTAssertNotNil(plugin) + } + + func testHandleMethodCall_canLaunch() { + let plugin = URLLauncherPlugin() + + let testCases = [[ + "url": "https://www.flutter.dev", + "expected": true + ], [ + "url": "invalidURL", + "expected": false + ]] + + for testCase in testCases { + let call = FlutterMethodCall(methodName: "canLaunch", arguments: ["url": testCase["url"]]) + let resultExpectation = expectation(description: "result block must be called.") + + plugin.handle(call) { result in + XCTAssertEqual( + result as? Bool, Optional(testCase["expected"]! as! Bool), + "result of canLaunch was not the expected result") + resultExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + } + + func testHandleMethodCall_launch() { + let plugin = URLLauncherPlugin() + let call = FlutterMethodCall(methodName: "launch", arguments: ["url": "https://flutter.dev"]) + let resultExpectation = expectation(description: "result block must be called.") + + plugin.handle(call) { result in + XCTAssertEqual( + result as? Bool, Optional(true), + "result block should be called with true on successful launch") + resultExpectation.fulfill() + + XCTAssertNil(plugin.currentSession) + } + + waitForExpectations(timeout: 1) + } + + func testHandleMethodCall_launchInVC() { + let plugin = URLLauncherPlugin() + let call = FlutterMethodCall(methodName: "launch", arguments: ["url": "https://flutter.dev", "useSafariVC": true]) + let resultExpectation = expectation(description: "result block must be called.") + + plugin.handle(call) { result in + XCTAssertEqual( + result as? Bool, Optional(true), + "result block should be called with true on successful launch") + resultExpectation.fulfill() + + XCTAssertNotNil(plugin.currentSession?.safari) + XCTAssertTrue(plugin.currentSession?.safari.isViewLoaded ?? false) + } + + waitForExpectations(timeout: 1) + } + + func testHandleMethodCall_closeWebView() { + let plugin = URLLauncherPlugin() + + // launch webview in separate VC + var call = FlutterMethodCall(methodName: "launch", arguments: ["url": "https://flutter.dev", "useSafariVC": true]) + var resultExpectation = expectation(description: "result block must be called.") + + plugin.handle(call) { result in + XCTAssertEqual( + result as? Bool, Optional(true), + "result block should be called with true on successful launch") + resultExpectation.fulfill() + + XCTAssertNotNil(plugin.currentSession?.safari) + } + + waitForExpectations(timeout: 1) + + // close webview + call = FlutterMethodCall(methodName: "close", arguments: nil) + resultExpectation = expectation(description: "result block must be called.") + + plugin.handle(call) { result in + XCTAssertEqual( + result as? Bool, nil, + "result block should be called with nil on close") + resultExpectation.fulfill() + + // test should pass if the safari VC is being dismissed or the + // currentSession has already been updated to nil + XCTAssertFalse(plugin.currentSession?.safari.isBeingDismissed ?? false) + } + + waitForExpectations(timeout: 1) + } + + func testHandleMethodCall_nonExistentMethod() { + let plugin = URLLauncherPlugin() + let call = FlutterMethodCall(methodName: "nonExistent", arguments: nil) + let resultExpectation = expectation(description: "result block must be called.") + + plugin.handle(call) { result in + XCTAssertEqual( + result as? NSObject, FlutterMethodNotImplemented, + "result block must be called with FlutterMethodNotImplemented") + resultExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } +} diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m deleted file mode 100644 index b6d3bceff039..000000000000 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m +++ /dev/null @@ -1,42 +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 XCTest; -@import os.log; - -@interface URLLauncherUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication *app; -@end - -@implementation URLLauncherUITests - -- (void)setUp { - self.continueAfterFailure = NO; - - self.app = [[XCUIApplication alloc] init]; - [self.app launch]; -} - -- (void)testLaunch { - XCUIApplication *app = self.app; - - NSArray *buttonNames = @[ - @"Launch in app", @"Launch in app(JavaScript ON)", @"Launch in app(DOM storage ON)", - @"Launch a universal link in a native app, fallback to Safari.(Youtube)" - ]; - for (NSString *buttonName in buttonNames) { - XCUIElement *button = app.buttons[buttonName]; - XCTAssertTrue([button waitForExistenceWithTimeout:30.0]); - XCTAssertEqual(app.webViews.count, 0); - [button tap]; - XCUIElement *webView = app.webViews.firstMatch; - XCTAssertTrue([webView waitForExistenceWithTimeout:30.0]); - XCTAssertTrue([app.buttons[@"ForwardButton"] waitForExistenceWithTimeout:30.0]); - XCTAssertTrue(app.buttons[@"Share"].exists); - XCTAssertTrue(app.buttons[@"OpenInSafariButton"].exists); - [app.buttons[@"Done"] tap]; - } -} - -@end diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift new file mode 100644 index 000000000000..bfd2d0b726ed --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift @@ -0,0 +1,40 @@ +// 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 +import XCTest + +class URLLauncherUITests: XCTestCase { + override func setUp() { + self.continueAfterFailure = false + + XCUIApplication().launch() + } + + func testLaunch() { + let app = XCUIApplication() + + let buttonNames = [ + "Launch in app", + "Launch in app(JavaScript ON)", + "Launch in app(DOM storage ON)", + "Launch a universal link in a native app, fallback to Safari. (Youtube)" + ] + + for buttonName in buttonNames { + let button = app.buttons[buttonName] + XCTAssertTrue(button.waitForExistence(timeout: 30.0)) + XCTAssertEqual(app.webViews.count, 0) + + button.tap() + let webView = app.webViews.firstMatch + XCTAssertTrue(webView.waitForExistence(timeout: 30.0)) + XCTAssertTrue(app.buttons["ForwardButton"].waitForExistence(timeout: 30.0)) + XCTAssertTrue(app.buttons["Share"].exists) + XCTAssertTrue(app.buttons["OpenInSafariButton"].exists) + + app.buttons["Done"].tap() + } + } +} diff --git a/packages/url_launcher/url_launcher_ios/example/lib/main.dart b/packages/url_launcher/url_launcher_ios/example/lib/main.dart index f01624ff87c6..a17025aee407 100644 --- a/packages/url_launcher/url_launcher_ios/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_ios/example/lib/main.dart @@ -162,6 +162,8 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { const String toLaunch = 'https://www.cylog.org/headers/'; + const String youtubeLink = 'https://www.youtube.com/watch?v=qYxRYB1oszw'; + return Scaffold( appBar: AppBar( title: Text(widget.title), @@ -175,8 +177,7 @@ class _MyHomePageState extends State { padding: const EdgeInsets.all(16.0), child: TextField( onChanged: (String text) => _phone = text, - decoration: const InputDecoration( - hintText: 'Input the phone number to launch')), + decoration: const InputDecoration(hintText: 'Input the phone number to launch')), ), ElevatedButton( onPressed: () => setState(() { @@ -214,14 +215,6 @@ class _MyHomePageState extends State { child: const Text('Launch in app(DOM storage ON)'), ), const Padding(padding: EdgeInsets.all(16.0)), - ElevatedButton( - onPressed: () => setState(() { - _launched = _launchUniversalLinkIos(toLaunch); - }), - child: const Text( - 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), - ), - const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewOrVC(toLaunch); @@ -232,6 +225,13 @@ class _MyHomePageState extends State { child: const Text('Launch in app + close after 5 seconds'), ), const Padding(padding: EdgeInsets.all(16.0)), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchUniversalLinkIos(youtubeLink); + }), + child: const Text('Launch a universal link in a native app, fallback to Safari. (Youtube)'), + ), + const Padding(padding: EdgeInsets.all(16.0)), FutureBuilder(future: _launched, builder: _launchStatus), ], ), 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 73589d2a0b7d..000000000000 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h +++ /dev/null @@ -1,8 +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 - -@interface FLTURLLauncherPlugin : NSObject -@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 375d5e2a2354..000000000000 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m +++ /dev/null @@ -1,165 +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 - -#import "FLTURLLauncherPlugin.h" - -@interface FLTURLLaunchSession : NSObject - -@property(copy, nonatomic) FlutterResult flutterResult; -@property(strong, nonatomic) NSURL *url; -@property(strong, nonatomic) SFSafariViewController *safari; -@property(nonatomic, copy) void (^didFinish)(void); - -@end - -@implementation FLTURLLaunchSession - -- (instancetype)initWithUrl:url withFlutterResult:result { - self = [super init]; - if (self) { - self.url = url; - self.flutterResult = result; - self.safari = [[SFSafariViewController alloc] initWithURL:url]; - self.safari.delegate = self; - } - return self; -} - -- (void)safariViewController:(SFSafariViewController *)controller - didCompleteInitialLoad:(BOOL)didLoadSuccessfully { - if (didLoadSuccessfully) { - self.flutterResult(@YES); - } else { - self.flutterResult([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 - -@interface FLTURLLauncherPlugin () - -@property(strong, nonatomic) FLTURLLaunchSession *currentSession; - -@end - -@implementation FLTURLLauncherPlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/url_launcher_ios" - binaryMessenger:registrar.messenger]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] init]; - [registrar addMethodCallDelegate:plugin channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - NSString *url = call.arguments[@"url"]; - if ([@"canLaunch" isEqualToString:call.method]) { - result(@([self canLaunchURL:url])); - } else if ([@"launch" isEqualToString:call.method]) { - NSNumber *useSafariVC = call.arguments[@"useSafariVC"]; - if (useSafariVC.boolValue) { - [self launchURLInVC:url result:result]; - } else { - [self launchURL:url call:call result:result]; - } - } else if ([@"closeWebView" isEqualToString:call.method]) { - [self closeWebViewWithResult:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (BOOL)canLaunchURL:(NSString *)urlString { - NSURL *url = [NSURL URLWithString:urlString]; - UIApplication *application = [UIApplication sharedApplication]; - return [application canOpenURL:url]; -} - -- (void)launchURL:(NSString *)urlString - call:(FlutterMethodCall *)call - result:(FlutterResult)result { - NSURL *url = [NSURL URLWithString:urlString]; - UIApplication *application = [UIApplication sharedApplication]; - - NSNumber *universalLinksOnly = call.arguments[@"universalLinksOnly"] ?: @0; - NSDictionary *options = @{UIApplicationOpenURLOptionUniversalLinksOnly : universalLinksOnly}; - [application openURL:url - options:options - completionHandler:^(BOOL success) { - result(@(success)); - }]; -} - -- (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result { - NSURL *url = [NSURL URLWithString:urlString]; - self.currentSession = [[FLTURLLaunchSession alloc] initWithUrl:url withFlutterResult:result]; - __weak typeof(self) weakSelf = self; - self.currentSession.didFinish = ^(void) { - weakSelf.currentSession = nil; - }; - [self.topViewController presentViewController:self.currentSession.safari - animated:YES - completion:nil]; -} - -- (void)closeWebViewWithResult:(FlutterResult)result { - if (self.currentSession != nil) { - [self.currentSession close]; - } - result(nil); -} - -- (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; -} -@end 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 000000000000..de88c4d1a004 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift @@ -0,0 +1,29 @@ +// 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 + +class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { + var url: URL + var safari: SFSafariViewController + var didFinish: (() -> Void)? + + init(url: URL) { + self.url = url + self.safari = SFSafariViewController(url: url) + super.init() + + self.safari.delegate = self + } + + func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + controller.dismiss(animated: true, completion: nil) + self.didFinish?() + } + + func close() { + self.safariViewControllerDidFinish(self.safari) + } +} 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 000000000000..bb3d0d72603b --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -0,0 +1,111 @@ +// 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 UIKit + +public final class URLLauncherPlugin: NSObject, FlutterPlugin { + var currentSession: URLLaunchSession? + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "plugins.flutter.io/url_launcher_ios", binaryMessenger: registrar.messenger()) + let instance = URLLauncherPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "canLaunch": + let args = call.arguments as! [String: Any] + let url = args["url"] as! String + result(canLaunchURL(url)) + case "launch": + let args = call.arguments as! [String: Any] + let url = args["url"] as! String + let useSafariVC = args["useSafariVC"] as? Bool ?? false + if useSafariVC { + launchURLInVC(url, result: result) + } else { + launchURL(url, call: call, result: result) + } + case "closeWebView": + closeWebView(result: result) + default: + result(FlutterMethodNotImplemented) + } + } + + private func canLaunchURL(_ url: String) -> Bool { + guard let url = URL(string: url) else { + return false + } + + return UIApplication.shared.canOpenURL(url) + } + + private func launchURL(_ url: String, call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as! [String: Any] + let universalLinksOnly = (args["universalLinksOnly"] as? Bool) ?? false + let options = [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: universalLinksOnly] + UIApplication.shared.open(URL(string: url)!, options: options) { success in + result(success) + } + } + + private func launchURLInVC(_ url: String, result: @escaping FlutterResult) { + currentSession = URLLaunchSession(url: URL(string: url)!) + currentSession?.didFinish = { + self.currentSession = nil + } + self.topViewController.present(currentSession!.safari, animated: true) { + result(true) + } + } + + private func closeWebView(result: @escaping FlutterResult) { + currentSession?.close() + result(nil) + } + + private func launchURLInVC(urlString: String, result: @escaping FlutterResult) { + let url = URL(string: urlString)! + self.currentSession = URLLaunchSession(url: url) + weak var weakSelf = self + self.currentSession!.didFinish = { + weakSelf?.currentSession = nil + } + self.topViewController.present(self.currentSession!.safari, animated: true, completion: nil) + } + + var topViewController: UIViewController { + // TODO: Provide a non-deprecated codepath. See https://github.com/flutter/flutter/issues/104117 + return topViewControllerFromViewController(UIApplication.shared.keyWindow!.rootViewController!) + } + + /// 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. + private func topViewControllerFromViewController(_ viewController: UIViewController) + -> UIViewController + { + if let navigationController = viewController as? UINavigationController { + return topViewControllerFromViewController(navigationController.viewControllers.last!) + } + if let tabController = viewController as? UITabBarController { + return topViewControllerFromViewController(tabController.selectedViewController!) + } + if let presentedViewController = viewController.presentedViewController { + return topViewControllerFromViewController(presentedViewController) + } + return viewController + } +} 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 9c265694018e..96689fee8a02 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 @@ -13,8 +13,12 @@ A Flutter plugin for making the underlying platform (Android or iOS) launch a UR s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_ios' } s.documentation_url = 'https://pub.dev/packages/url_launcher' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' + s.swift_version = '5.0' + 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', + } s.dependency 'Flutter' s.platform = :ios, '11.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml index 5a5c4bdc0514..45f0e73ed13b 100644 --- a/packages/url_launcher/url_launcher_ios/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_ios description: iOS implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/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.1.0 +version: 6.1.1 environment: sdk: '>=2.18.0 <3.0.0' @@ -13,7 +13,7 @@ flutter: implements: url_launcher platforms: ios: - pluginClass: FLTURLLauncherPlugin + pluginClass: URLLauncherPlugin dartPluginClass: UrlLauncherIOS dependencies: