在今年的 WWDC25 上,Apple 發布 TN3187 文檔,其中明確了要求:“在 iOS 26 之後的版本,任何使用最新 SDK 構建的 UIKit 應用都必須使用 UIScene 生命週期,否則將無法啟動”:
實際上 UIScene 不是什麼新鮮東西,反而是一個老古董,畢竟它是在 iOS 13 中引入的,它的核心思想是將應用的“進程”生命週期和“UI 實例”的生命週期分離,讓應用可以同時管理多個獨立的 UI 實例。
而在此之前,iOS 主要圍繞單體模型 UIApplicationDelegate 來實現生命週期管理,例如:
application(_:didFinishLaunchingWithOptions:) / applicationWillTerminate(_:)applicationDidBecomeActive(_)) 或進入背景 (applicationDidEnterBackground(_))AppDelegate 擁有並管理著應用唯一的 UIWindow 實例所以可以明顯看到,這種單體模型的架構最根本的缺陷在於,將應用進程與 UI 界面緊密綁定,導致整個應用只有一個統一的 UI 狀態。
但是這在之前對於 Flutter 來說並沒有什麼問題,因為 Flutter 默認本身就是一個單頁面的架構,雖然存在 UIScene ,但是 AppDelegate 就滿足需求了,所以在本次遷移到 UIScene 生命週期之前,Flutter 在 iOS 平台上的整個原生集成都圍繞著 UIApplicationDelegate 構建,而隨著本次 TN3187 的要求,Flutter 不得不開始完全遷移到 UIScene 模型。
對於 UIScene 模型,整個邏輯主要包括三個概念:
UIScene:代表應用 UI 的一個獨立實例,絕大多數情況下開發者熟悉的就是 UIWindowScene,它管理著一個或多個窗口以及相關的 UIUISceneSession:持久化對象,它代表一個場景的配置和狀態,比如即使其對應的 UIScene 實例因為資源回收等原因被系統斷開連接或銷毀,UISceneSession 仍然存在,保存著恢復該場景所需的信息,是實現狀態恢復的關鍵UISceneDelegate:作為 UIScene 的代理,它專門負責管理特定場景的生命週期事件,例如連接、斷開、進入前台、進入背景等所以到這裡,可以很明顯看出來,UIApplicationDelegate 和 UISceneDelegate 有了進一步的明顯分割:
UIApplicationDelegate :處理進程級別的事件,比如應用啟動和終止的,並負責處理推送通知的註冊等全局任務UISceneDelegate :接管了所有與 UI 相關的生命週期管理,包括場景的創建與連接 (scene(_:willConnectTo:options:)),活躍 (sceneDidBecomeActive(_));進入背景 (sceneDidEnterBackground(_));以及斷開連接 (sceneDidDisconnect(_)) 等具體大概會是以下的關係變化:
| AppDelegate | SceneDelegate | 新增 | 範圍與職責轉移 | 關鍵行為差異 |
|---|---|---|---|---|
application(_:didFinishLaunchingWithOptions:) |
scene(_:willConnectTo:options:) |
application(_:configurationForConnecting:options:) |
從 AppDelegate 轉移到 SceneDelegate,AppDelegate 仍處理非 UI 的全局初始化(如三方庫配置),SceneDelegate 負責創建 UIWindow 和設置根視圖控制器 |
AppDelegate 的 didFinishLaunchingWithOptions 在應用冷啟動時僅調用一次,SceneDelegate 的 willConnectTo 在每個場景(窗口)創建時都會調用。 |
applicationDidBecomeActive(_) |
sceneDidBecomeActive(_) |
- | 從應用級轉移到場景級,AppDelegate 的方法在場景模型下不再被調用 |
sceneDidBecomeActive 針對單個場景,允許對不同窗口進行獨立的激活處理 |
applicationWillResignActive(_) |
sceneWillResignActive(_) |
- | 從應用級轉移到場景級,AppDelegate 的方法在場景模型下不再被調用 |
sceneWillResignActive 針對單個場景,例如當一個窗口被另一個應用(如 Slide Over)遮擋時觸發 |
applicationDidEnterBackground(_) |
sceneDidEnterBackground(_) |
- | 從應用級轉移到場景級,AppDelegate 的方法在場景模型下不再被調用 |
sceneDidEnterBackground 允許對每個場景的狀態進行獨立保存。 |
applicationWillEnterForeground(_) |
sceneWillEnterForeground(_) |
- | 從應用級轉移到場景級,AppDelegate 的方法在場景模型下不再被調用 |
sceneWillEnterForeground 在應用冷啟動時也會被調用,而 applicationWillEnterForeground 不會。這是遷移過程中常見的邏輯錯誤來源 |
application(_:open:options:) |
scene(_:openURLContexts:) |
- | 從應用級轉移到場景級,AppDelegate 的方法在場景模型下不再被調用 |
scene(_:openURLContexts:) 接收到的 URL 會被路由到最合適的場景進行處理 |
application(_:continue:restoreHandler:) |
scene(_:continue:) |
- | 從應用級轉移到場景級 | scene(_:continue:) 允許為特定場景恢復用戶活動狀態 |
applicationWillTerminate(_) |
sceneDidDisconnect(_) |
application(_:didDiscardSceneSessions:) |
applicationWillTerminate 仍表示整個應用的終止,sceneDidDisconnect 表示場景被系統回收資源(可能重連),didDiscardSceneSessions 表示用戶通過應用切換器關閉了場景(永久銷毀) |
職責更加細化,sceneDidDisconnect 不等於應用終止,而 didDiscardSceneSessions 是清理被用戶主動關閉的場景資源的入口。 |
application(_:didReceiveRemoteNotification:fetchCompletionHandler:) |
- | - | 職責保留在 AppDelegate,推送通知是進程級事件,不與特定 UI 實例綁定 |
即使在場景模型下,推送通知的接收和處理邏輯仍然主要位於 AppDelegate |
而對於 Flutter Framework 層面的變化,主要有:
UIApplication 的全局通知來暫停或恢復渲染,而遷移後必須改為監聽基於單個 UIScene 的通知,以正確處理多窗口下的渲染暫停和恢復UIApplication.shared.keyWindow API 來獲取應用的窗口,這些調用都必須被替換FlutterViewController 的創建時機發生變化,插件的註冊和關聯 FlutterEngine 的機制也需要重構,確保在正確的時機與正確的引擎實例關聯而對於 Flutter 插件來說, 任何依賴於 UI 生命週期事件或需要與 UI 窗口交互的插件都可能受到影響,Flutter 官方對第一方插件進行了大規模的遷移:
url_launcher_ios:需要獲取當前窗口來呈現瀏覽器視圖local_auth_darwin:進行生物識別認證時需要與 UI 交互image_picker_ios:需要呈現圖片選擇介面google_sign_in_ios:需要彈出登錄窗口quick_actions_ios:處理主螢幕快捷操作,其回調方法從 AppDelegate 轉移到了 SceneDelegate
而對於 Flutter 應用開發者,Flutter 提供了一條自動化和通用的手動遷移方式:
自動化遷移(推薦):如果你的 Flutter 項目的原生 iOS 部分(ios 文件夾)沒有經過大量定制化修改,可以使用 Flutter CLI 提供的實驗性功能來自動完成遷移。
UIScene 自動遷移開關
flutter config --enable-uiscene-migrationflutter build ios
///or
flutter runAppDelegate.swift(或 .m),移除過時的 UI 生命週期回調ios/Runner/ 目錄下創建一個新的 SceneDelegate.swift(或 .h/.m)文件繼承自 FlutterSceneDelegateInfo.plist 文件,添加必要的 UIApplicationSceneManifest 配置手動遷移:對於那些有複雜原生代碼、自定義 AppDelegate 或其他特殊配置的應用,需要手動遷移:
AppDelegate.swift:
ios/Runner/AppDelegate.swift,刪除所有與 UI 生命週期相關的方法,例如 applicationDidBecomeActive、applicationWillResignActive、applicationDidEnterBackground、applicationWillEnterForeground (可以參考前面的表格)application(_:didFinishLaunchingWithOptions:) 方法,但確保其中只包含應用級的初始化邏輯(如註冊插件、配置三方服務),移除所有創建和設置 window 的代碼AppDelegate 類繼承自 FlutterAppDelegate(如果之前不是的話),或者遵循 FlutterAppLifeCycleProvider 協議創建 SceneDelegate.swift:
Runner 文件夾,選擇 "New File..." -> "Swift File",命名為 SceneDelegate.swiftSceneDelegate,它繼承 FlutterSceneDelegate,從而自動獲得了將場景生命週期事件橋接到 Flutter 引擎的能力
import UIKit
import Flutterclass SceneDelegate: FlutterSceneDelegate {
// 你可以在這裡重寫 FlutterSceneDelegate 的方法
// 來添加自定義的場景生命週期邏輯。
}
Info.plist:
ios/Runner/Info.plist,在根 dict 標籤內,添加以下 UIApplicationSceneManifest :
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>didFinishLaunchingWithOptions 中有創建 Method Channels 或 Platform Views 的邏輯,這些邏輯都需要遷移,因為在 didFinishLaunchingWithOptions 執行時,FlutterViewController 可能還不存在SceneDelegate 的 scene(_:willConnectTo:options:) 方法,或者創建一個專門的初始化方法,在場景連接後調用,Flutter 的建議將這類邏輯移至 didInitializeImplicitFlutterEngine 方法最後就是“天見猶憐”的插件開發者,對於插件作者而言 UIScene 遷移帶來了更大的挑戰:必須確保插件既能在已經遷移到 UIScene 的新應用中正常工作,也要能在尚未遷移的舊應用或舊版 iOS 系統上保持兼容,例如:
AppDelegate 移到 SceneDelegate,這樣做會導致它在未遷移的應用中完全失效,因此插件必須能夠同時處理兩種生命週期模型register(with registrar: FlutterPluginRegistrar) 方法中,除了像以前一樣通過 registrar.addApplicationDelegate(self) 註冊 AppDelegate 事件監聽外,還需要調用新的 API 來註冊 SceneDelegate 事件的監聽,Flutter 提供了相應的機制讓插件可以接收到場景生命週期的回調UISceneDelegate 協議中的相關方法,在實現時要設計一種優雅降級的邏輯。例如同時實現 applicationDidEnterBackground 和 sceneDidEnterBackground,當 sceneDidEnterBackground 被調用時,執行相應邏輯並設置一個標誌位,以避免 applicationDidEnterBackground 中的邏輯重複執行(如果它也被意外調用的話)UIApplication.shared.keyWindow 或其他與單一窗口相關的廢棄 API 的調用都必須被替換例如 url_launcher_ios 插件的遷移: ,在 UIScene 之前,當需要彈出一個外部瀏覽器窗口時,它可能需要獲取應用的 keyWindow 作為視圖層級的參考:
// 遷移前
if let window = UIApplication.shared.keyWindow {
// Use window to present something...
}
// 遷移後
// 透過 registrar 訪問窗口,這是場景意識的。
if let window = self.pluginRegistrar.view?.window {
// 使用場景特定的窗口...
}
// 在基於場景的應用中尋找關鍵窗口的更健壯的方法
let keyWindow = self.pluginRegistrar.view?.window?.windowScene?.keyWindow
這個例子可以看到,插件從直接訪問全局單例 UIApplication.shared.keyWindow,轉變為通過與插件關聯的 pluginRegistrar 來獲取視圖 (view),再從該視圖向上追溯到其所在的 window 和 windowScene,最終找到正確的窗口。
所以對於插件開發者來說,需要適配不同版本的 Flutter 來完成工作,無疑加大了成本。
這其實也在一定程度來自於歷史技術債務,因為其實 UIScene 是很早前就存在的 API ,但是由於 Flutter 場景的特殊性,默認 UIApplicationDelegate 一直滿足需求,而面對這次 iOS 的強制調整,歷史債務就很明顯的爆發出來,特別是對於社區第三方開發者的適配成本。
不過好消息是,我們還有時間,而全新的 Flutter 3.38.0-0.1.pre 也才剛剛出來,但是這對 Flutter 下個版本的穩定性也是一個挑戰,因為這也是一個底層較大重構。