將 Flutter module 整合到 iOS 專案

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

  1. Use the CocoaPods dependency manager and installed Flutter SDK. In this case, the flutter_module is compiled from the source each time the app is built. (Recommended.)

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

  1. Create frameworks for the Flutter engine, your compiled Dart code, and all Flutter plugins. Here, you manually embed the frameworks, and update your existing application’s build settings in Xcode. This can be useful for teams that don’t want to require every developer to have the Flutter SDK and Cocoapods installed locally.

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

  1. Create frameworks for your compiled Dart code, and all Flutter plugins. Use CocoaPods for the Flutter engine. With this option, embed the frameworks for your application and the plugins in Xcode, but distribute the Flutter engine as a CocoaPods podspec. This is similar to the second option, but it provides an alternative to distributing the large Flutter.xcframework.

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

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

系統要求

你的開發環境必須滿足 Flutter 對 macOS 系統的版本要求已經安裝 Xcode,Flutter 支援 iOS 11 及以上。此外,你還需要 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 中新增下面程式碼:

    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)

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

    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

內嵌框架

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

After linking the frameworks, you should see them in the Frameworks, Libraries, and Embedded Content section of your target’s General settings. To embed the dynamic frameworks select Embed & Sign.

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

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

Host apps using CocoaPods can add Flutter to their Podfile:

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

Link and embed the generated App.xcframework, FlutterPluginRegistrant.xcframework, and any plugin frameworks into your existing application as described in Option B.

Local Network Privacy Permissions

On iOS 14 and higher, enable the Dart multicast DNS service in the Debug version of your app to add debugging functionalities such as hot-reload and DevTools via flutter attach.

One way to do this is to maintain a separate copy of your app’s Info.plist per build configuration. The following instructions assume the default Debug and Release. Adjust the names as needed depending on your app’s build configurations.

  1. Rename your app’s Info.plist to Info-Debug.plist. Make a copy of it called Info-Release.plist and add it to your Xcode project.

    Info-Debug.plist and Info-Release.plist in Xcode
  2. In Info-Debug.plist only add the key NSBonjourServices and set the value to an array with the string _dartobservatory._tcp. Note Xcode will display this as “Bonjour services”.

    Optionally, add the key NSLocalNetworkUsageDescription set to your desired customized permission dialog text.

    Info-Debug.plist with additional keys
  3. In your target’s build settings, change the Info.plist File (INFOPLIST_FILE) setting path from path/to/Info.plist to path/to/Info-$(CONFIGURATION).plist.

    Set INFOPLIST_FILE build setting

    This will resolve to the path Info-Debug.plist in Debug and Info-Release.plist in Release.

    Resolved INFOPLIST_FILE build setting

    Alternatively, you can explicitly set the Debug path to Info-Debug.plist and the Release path to Info-Release.plist.

  4. If the Info-Release.plist copy is in your target’s Build Settings > Build Phases > Copy Bundle Resources build phase, remove it.

    Copy Bundle build phase

    The first Flutter screen loaded by your Debug app will now prompt for local network permission. The permission can also be allowed by enabling Settings > Privacy > Local Network > Your App.

    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 頁面 到你的既有應用中。