將 Flutter module 整合到 iOS 專案

Flutter UI 元件可以漸進式地內嵌到你現有的 iOS 應用中,下面是幾種方法:

  1. 使用 CocoaPods 依賴管理器安裝 Flutter SDK 使用這種方法,每次建立應用的時候都會從原始碼中編譯 flutter_module。(推薦)

  2. 建立一個框架,把 Flutter 引擎、已編譯的 Dart 程式碼和所有 Flutter 外掛都放進去 這種方式你可以手動嵌入這個框架,並在 Xcode 中更改現有的應用的建立設定。如果不想要求開發團隊的每一位成員都在本地安裝 Flutter SDK 和 Cocoapods,這種方式比較適用。

  3. 為已編譯的 Dart 程式碼和所有 Flutter 外掛建立一個框架,對 Flutter 引擎使用 CocoaPods 來管理 這種方式是將應用內容和外掛作為內嵌的框架,但將 Flutter 引擎作為 CocoaPods podspec 分發。這有點類似第二種方式,但是它為分發大型的 Flutter.xcframework 檔案提供了替代方案。

例如使用 UIKit 建立的應用,請參閱 add_to_app 程式碼範例 中 iOS 這個目錄。有關使用 SwiftUI 的範例,請參閱 News Feed App 中的 iOS 目錄。

系統要求

你的開發環境必須滿足 Flutter 對 macOS 系統的版本要求已經安裝 Xcode,Flutter 支援 iOS 12 及以上。此外,你還需要 1.10 或以上版本的 CocoaPods

建立 Flutter module

為了將 Flutter 整合到你的既有應用裡,參考上面的任意方法先建立一個 Flutter module。

在指令行中執行:

cd some/path/
flutter create --template module my_flutter

Flutter module 會建立在 some/path/my_flutter/ 目錄。如果你使用上述第一種方法,則應在與現有 iOS 應用工程的父目錄中建立這個 Flutter module。

在這個目錄中,你可以像在其它 Flutter 專案中一樣,執行 flutter 指令。比如 flutter run --debug 或者 flutter build ios。同樣,你也可以透過 Android Studio/IntelliJ 或者 VS Code 中的 Flutter 和 Dart 外掛執行這個 module,在整合到現有應用前,這個專案在 Flutter module 中包含了一個單檢視的範例程式碼,對 Flutter 側程式碼的測試會有幫助。

Module 的目錄結構

my_flutter module 裡,目錄結構和普通 Flutter 應用類似:

my_flutter/
├── .ios/
│   ├── Runner.xcworkspace
│   └── Flutter/podhelper.rb
├── lib/
│   └── main.dart
├── test/
└── pubspec.yaml

新增你的 Dart 程式碼到 lib/ 目錄。

新增 Flutter 依賴到 my_flutter/pubspec.yaml,包括 Flutter packages 和 plugins。

.ios/ 隱藏檔案夾包含了一個 Xcode workspace,用於單獨執行你的 Flutter module。它是一個獨立啟動 Flutter 程式碼的殼工程,並且包含了一個幫助指令碼,用於編譯 framewroks 或者使用 CocoaPods 將 Flutter module 整合到你的既有應用。

在你的既有應用中整合 Flutter module

在你的 module 開發完成後,你就能使用頁面頂部描述的方法將其嵌入到應用中去了。

使用 Flutter 會 增加應用體積

選項 A - 使用 CocoaPods 和 Flutter SDK 整合

這個方法需要你的專案的所有開發者,都在本地安裝 Flutter SDK。你的工程在每次建立的的時候,都將會從原始碼裡編譯 Flutter 模組。只需要在 Xcode 中編譯應用,就可以自動執行指令碼來整合 Dart 程式碼和外掛。這個方法允許你使用 Flutter module 中的最新程式碼快速迭代開發,而無需在 Xcode 以外執行額外的指令。

下面的範例假設你的既有應用和 Flutter module 在相鄰目錄。如果你有不同的目錄結構,需要適配到對應的路徑。

some/path/
├── my_flutter/
│   └── .ios/
│       └── Flutter/
│         └── podhelper.rb
└── MyApp/
    └── Podfile

如果你的應用下(MyApp)還沒有 Podfile,請執行 pod init 來建立一個。你可以在 CocoaPods 起步指南 中瞭解更多。

  1. Podfile 中新增下面程式碼:

    MyApp/Podfile
    flutter_application_path = '../my_flutter'
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
  2. 每個需要整合 Flutter 的 Podfile target,執行 install_all_flutter_pods(flutter_application_path)

    MyApp/Podfile
    target 'MyApp' do
      install_all_flutter_pods(flutter_application_path)
    end
  3. Podfilepost_install 部分,呼叫 flutter_post_install(installer)

    MyApp/Podfile
    post_install do |installer|
      flutter_post_install(installer) if defined?(flutter_post_install)
    end
  4. 執行 pod install

podhelper.rb 指令碼會把你的 plugins, Flutter.framework,和 App.framework 整合到你的專案中。

你應用的 Debug 和 Release 編譯設定,將會整合相對應的 Debug 或 Release 的 編譯產物。可以增加一個 Profile 編譯設定用於在 profile 模式下測試應用。

在 Xcode 中開啟 MyApp.xcworkspace ,你現在可以使用 ⌘B 編譯專案了。

選項 B - 在 Xcode 中整合 frameworks

除了上面的方法,你也可以建立必備的 frameworks,手動修改既有 Xcode 專案,將他們整合進去。當你組內其它成員們不能在本地安裝 Flutter SDK 和 CocoaPods,或者你不想使用 CocoaPods 作為既有應用的依賴管理時,這種方法會比較合適。但是每當你在 Flutter module 中改變了程式碼,都必須執行 flutter build ios-framework

下面的範例假設你想在 some/path/MyApp/Flutter/ 目錄下建立 frameworks:

flutter build ios-framework --output=some/path/MyApp/Flutter/
some/path/MyApp/
└── Flutter/
    ├── Debug/
    │   ├── Flutter.xcframework
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework (only if you have plugins with iOS platform code)
    │   └── example_plugin.xcframework (each plugin is a separate framework)
    ├── Profile/
    │   ├── Flutter.xcframework
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework
    │   └── example_plugin.xcframework
    └── Release/
        ├── Flutter.xcframework
        ├── App.xcframework
        ├── FlutterPluginRegistrant.xcframework
        └── example_plugin.xcframework

在 Xcode 中將生成的 frameworks 整合到你的既有應用中。例如,你可以在 some/path/MyApp/Flutter/Release/ 目錄拖拽 frameworks 到你的應用 target 編譯設定的 General > Frameworks, Libraries, and Embedded Content 下,然後在 Embed 下拉清單中選擇 “Embed & Sign”。

例如,你可以將框架從 Finder 的 some/path/MyApp/Flutter/Release/ 拖到你的目標專案中,然後點選以下步驟 build settings > Build Phases > Link Binary With Libraries。

在 target 的編譯設定中的 Framework Search Paths (FRAMEWORK_SEARCH_PATHS) 增加 $(PROJECT_DIR)/Flutter/Release/

Update Framework Search Paths in Xcode

Embed the frameworks

內嵌框架

生成的動態框架必須嵌入你的應用並且在執行時被載入。

例如,你可以從應用框架組中拖拽框架(除了 FlutterPluginRegistrant 以及其他的靜態框架)到你的目標 ‘ build settings > Build Phases > Embed Frameworks。然後從下拉選單中選擇『Embed & Sign』。

之後它們將出現在 Build Phases 中的 Embed Frameworks 內,如下所示:

Embed frameworks in Xcode

你現在可以在 Xcode中使用 ⌘B 編譯專案。

選項 C - 使用 CocoaPods 在 Xcode 和 Flutter 框架中內嵌應用和外掛框架

除了將一個很大的 Flutter.framework 分發給其他開發者、機器或者持續整合 (CI) 系統之外,你可以加入一個引數 --cocoapods 將 Flutter 框架作為一個 CocoaPods 的 podspec 檔案分發。這將會生成一個 Flutter.podspec 檔案而不再生成 Flutter.framework 引擎檔案。如選項 B 中所說的那樣,它將會生成 App.framework 和外掛框架。

要生成 Flutter.podspec 和框架,指令行切換到 Flutter module 根目錄,然後執行以下指令:

flutter build ios-framework --cocoapods --output=some/path/MyApp/Flutter/
some/path/MyApp/
└── Flutter/
    ├── Debug/
    │   ├── Flutter.podspec
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework
    │   └── example_plugin.xcframework (each plugin with iOS platform code is a separate framework)
    ├── Profile/
    │   ├── Flutter.podspec
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework
    │   └── example_plugin.xcframework
    └── Release/
        ├── Flutter.podspec
        ├── App.xcframework
        ├── FlutterPluginRegistrant.xcframework
        └── example_plugin.xcframework

使用 CocoaPods 的宿主應用程式可以將 Flutter 新增到 Podfile 中:

MyApp/Podfile
pod 'Flutter', :podspec => 'some/path/MyApp/Flutter/[build mode]/Flutter.podspec'

如選項 B 所述,將生成的 App.xcframework、 FlutterPluginRegistrant.xcframework 以及任何外掛框架,連結並嵌入到你現有的應用程式中。

本地網路隱私權限

在 iOS 14 及更高的版本中,可以在應用程式的 Debug 版本中啟用 Dart 的多播 DNS 服務 (multicast DNS service),透過 flutter attach 新增 除錯功能,如熱過載和 DevTools

還有一種方式是將每種不同的建立設定,單獨建立對應設定的 Info.plist。下面的說明假定預設為 除錯版本 (Debug)發布版本 (Release)。根據應用程式建立設定的需要調整名稱。

  1. 將應用程式中的 Info.plist 重新命名為 Info-Debug.plist,再複製一個相同的檔案並重命名為 Info-Release.plist,最後將 Info-Debug.plistInfo-Release.plist 新增到 Xcode 專案中。

    Info-Debug.plist and Info-Release.plist in Xcode
  2. Info-Debug.plist 新增 key NSBonjourServices,並將它的值設定為陣列 (Array),然後在該陣列中新增 _dartVmService._tcp 字串 (String)。

    可以選擇新增 key NSLocalNetworkUsageDescription,並設定為你自定義的許可權提示對話框文字。

    Info-Debug.plist with additional keys
  3. 在 target 建立設定中,將 Info.plist File (INFOPLIST_FILE) 設定路徑從 path/to/Info.plist 改為 path/to/Info-$(CONFIGURATION).plist

    Set INFOPLIST_FILE build setting

    這個設定將會在 Debug 時,使用 Info-Debug.plist 的設定,在 Release 時,使用 Info-Release.plist 的設定。

    Resolved INFOPLIST_FILE build setting

    又或者,你可以明確地將 Debug 的路徑設定為 Info-Debug.plist,將 Release 的路徑設定為 Info-Release.plist

  4. 如果 Info-Release.plist 在 target 中 Build Settings > Build Phases > Copy Bundle Resources 的時候,請刪除它。

    Copy Bundle build phase

    現在 Debug 應用程式會在 Flutter 啟動時提示本地網路許可權。也可以透過開啟 設定 > 隱私與安全性 > 本地網路 > 你的應用程式 來允許該許可權。

    Local network permission dialog

Apple Silicon (arm64 Macs)

在使用 Apple Silicon 晶片的 Mac 上 (M1),宿主應用將針對 arm64 架構的模擬器編譯。儘管 Flutter 支援 arm64 的 iOS 模擬器,但一些外掛仍有可能未進行支援。當你在使用這些外掛時,你會遇到 Undefined symbols for architecture arm64 的錯誤,此時你必須從模擬器支援架構中移除 arm64

在宿主應用的 Target 中,找到名為 Excluded Architectures (EXCLUDED_ARCHS) 的建立設定。單擊右側的箭頭指示器圖示以展開可用的建立設定。將滑鼠懸停在 Debug 處並單擊加號圖示。將 Any SDK 更改為 Any iOS Simulator SDK。然後向建立設定值中新增 arm64

Set conditional EXCLUDED_ARCHS build setting

當全部都正確設定後,Xcode 將會向你的 project.pbxproj 檔案中新增 "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;

然後對全部 iOS 目標再次執行單元測試。

開發

你現在可以 新增一個 Flutter 頁面 到你的既有應用中。