Skip to content

Flutter - 编写 iOS 特定代码

与 Android 类似,Flutter 允许使用平台通道(Platform Channels)与原生 iOS 代码(用 Swift 或 Objective-C 编写)进行通信。这使得可以访问 iOS 特定的 API、框架(frameworks)和第三方原生 SDK。

架构(客户端 Client/宿主 Host、MethodChannel、消息编解码器 Message Codec、异步通信 Async communication)与之前描述的 Android 实现相同。主要区别在于宿主(iOS)端使用的原生语言和 API。

我们将实现相同的“打开浏览器”功能,这次针对 iOS。(同样,请记住 url_launcher 包简化了这个特定任务,但此示例是为了说明手动平台通道过程。)

  1. 先决条件: 你需要一台安装了 Xcode 的 macOS 机器来构建和运行 iOS 部分。
  2. 创建项目: 运行 flutter create ios_platform_channel_app。
  3. Dart 代码(lib/main.dart): Dart 代码与 Android 示例中的完全相同。MethodChannel 名称(com.example.ios_platform_channel_app/browser - 根据你的组织/应用进行调整)和方法调用(openBrowser 带 url 参数)是平台无关的。
// --- Dart 代码与 Android 示例相同 ---
// --- 确保 MethodChannel 名称与下方使用的名称匹配 ---
// static const platform = MethodChannel('com.example.ios_platform_channel_app/browser');
// ... rest of main.dart from Android example ...
  1. iOS 原生代码 (Swift - 推荐):
    • 在 Xcode 中打开 iOS 模块:在 Finder 中右键点击 Flutter 项目中的 ios 文件夹,选择 Open in Xcode(在 Xcode 中打开),或在终端中使用 open ios/Runner.xcworkspace。
    • 导航到 Runner > Runner > AppDelegate.swift。
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 1. 获取 FlutterViewController
// 获取 controller 对于访问 binary messenger 至关重要
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController is not type FlutterViewController")
}
// 2. 定义与 Dart 中使用的相同通道名称
let channelName = "com.example.ios_platform_channel_app/browser"
let browserChannel = FlutterMethodChannel(name: channelName,
binaryMessenger: controller.binaryMessenger)
// 3. 设置 MethodCallHandler
browserChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
// 当 Dart 调用 invokeMethod 时,此闭包被调用
// 4. 检查调用了哪个方法
guard call.method == "openBrowser" else {
result(FlutterMethodNotImplemented)
return
}
// 5. 提取参数
// 参数通常以 Dictionary 形式传递
if let args = call.arguments as? [String: Any],
let urlString = args["url"] as? String {
// 6. 调用原生函数
self?.openBrowser(urlString: urlString, result: result)
} else {
result(FlutterError(code: "INVALID_ARGUMENT",
message: "URL argument is missing or not a string",
details: nil))
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// 7. 打开 URL 的原生函数
private func openBrowser(urlString: String, result: FlutterResult) {
guard let url = URL(string: urlString) else {
result(FlutterError(code: "INVALID_URL",
message: "Could not parse URL: \(urlString)",
details: nil))
return
}
// 在尝试打开之前检查 URL 是否可以打开
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { (success) in
// 打开尝试后将结果发送回 Dart
result(success)
}
} else {
result(FlutterError(code: "CANNOT_OPEN_URL",
message: "Cannot open URL: \(urlString)",
details: nil))
}
}
}
  1. (备选方案) iOS 原生代码 (Objective-C): 如果你的项目使用 Objective-C,编辑 Runner > Runner > AppDelegate.m。
#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 1. 获取 FlutterViewController
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
// 2. 定义与 Dart 中使用的相同通道名称
NSString* channelName = @"com.example.ios_platform_channel_app/browser";
FlutterMethodChannel* browserChannel = [FlutterMethodChannel
methodChannelWithName:channelName
binaryMessenger:controller.binaryMessenger];
// 3. 设置 MethodCallHandler
__weak typeof(self) weakSelf = self;
[browserChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
// 当 Dart 调用 invokeMethod 时,此块被调用
// 4. 检查调用了哪个方法
if ([@"openBrowser" isEqualToString:call.method]) {
// 5. 提取参数
// 参数通常以 Dictionary 形式传递
NSDictionary* args = call.arguments;
NSString* urlString = args[@"url"];
if (urlString != nil && [urlString isKindOfClass:[NSString class]]) {
// 6. 调用原生函数
[weakSelf openBrowserWithUrlString:urlString result:result];
} else {
result([FlutterError errorWithCode:@"INVALID_ARGUMENT"
message:@"URL argument is missing or not a string"
details:nil]);
}
} else {
result(FlutterMethodNotImplemented);
}
}];
[GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
// 7. 打开 URL 的原生函数
- (void)openBrowserWithUrlString:(NSString*)urlString result:(FlutterResult)result {
NSURL* url = [NSURL URLWithString:urlString];
if (url == nil) {
result([FlutterError errorWithCode:@"INVALID_URL"
message:[NSString stringWithFormat:@"Could not parse URL: %@", urlString]
details:nil]);
return;
}
UIApplication* application = [UIApplication sharedApplication];
if ([application canOpenURL:url]) {
[application openURL:url options:@{} completionHandler:^(BOOL success) {
// 打开尝试后将结果发送回 Dart
result(@(success));
}];
} else {
result([FlutterError errorWithCode:@"CANNOT_OPEN_URL"
message:[NSString stringWithFormat:@"Cannot open URL: %@", urlString]
details:nil]);
}
}
@end
  1. 配置 Info.plist(如果打开非 http/https URL): 如果你需要打开自定义 scheme 的 URL(例如 mailto: 或应用特定的 scheme),你需要在 ios/Runner/Info.plist 中的 LSApplicationQueriesSchemes 数组中添加它们。对于 http/https 这不是必需的。
  2. 运行应用: 在 IDE 中选择一个 iOS 模拟器或连接的物理 iOS 设备。停止任何之前的运行并重新启动应用(flutter run)。点击“Open Flutter Dev”按钮。它应该会启动 Safari 并导航到 https://flutter.dev。
  • 语言选择: 通常推荐使用 Swift 进行新的 iOS 开发,因为它具有现代特性和安全性。
  • 错误处理: 在 Dart (PlatformException) 和原生端(使用 FlutterError 返回结果)都实现健壮的错误处理。
  • 线程安全 (iOS): 平台通道处理程序在主线程(main thread)上执行。将长时间运行的任务转移到后台队列(background queues),使用 Grand Central Dispatch (GCD) 来避免阻塞 UI。
  • API 可用性: 确保你调用的原生 API 在目标 iOS 版本上可用。
  • 权限: 访问某些原生功能(如相机 camera、位置 location、联系人 contacts)需要通过 Info.plist 键请求用户许可,并且可能需要使用平台通道或包来触发许可对话框。
  • 包: 与 Android 一样,始终首先检查 pub.dev 是否存在可能已经提供所需原生功能的现有包(如 url_launcher)。