🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

iOS 26 開始強制使用 UIScene,你的 Flutter 插件準備好遷移支持了嗎?

在今年的 WWDC25 上,Apple 發布 TN3187 文檔,其中明確了要求:“在 iOS 26 之後的版本,任何使用最新 SDK 構建的 UIKit 應用都必須使用 UIScene 生命週期,否則將無法啟動”:

實際上 UIScene 不是什麼新鮮東西,反而是一個老古董,畢竟它是在 iOS 13 中引入的,它的核心思想是將應用的“進程”生命週期和“UI 實例”的生命週期分離,讓應用可以同時管理多個獨立的 UI 實例。

而在此之前,iOS 主要圍繞單體模型 UIApplicationDelegate 來實現生命週期管理,例如:

  • 負責處理應用進程的啟動與終止 application(_:didFinishLaunchingWithOptions:) / applicationWillTerminate(_:)
  • 所有與 UI 狀態相關的事件,例如應用進入前台並變得活躍 (applicationDidBecomeActive(_)) 或進入背景 (applicationDidEnterBackground(_))
  • 窗口管理 AppDelegate 擁有並管理著應用唯一的 UIWindow 實例
  • 處理系統級事件,包括響應遠程推送通知、處理通過 URL Scheme 如 Deeplink 等

所以可以明顯看到,這種單體模型的架構最根本的缺陷在於,將應用進程與 UI 界面緊密綁定,導致整個應用只有一個統一的 UI 狀態。

但是這在之前對於 Flutter 來說並沒有什麼問題,因為 Flutter 默認本身就是一個單頁面的架構,雖然存在 UIScene ,但是 AppDelegate 就滿足需求了,所以在本次遷移到 UIScene 生命週期之前,Flutter 在 iOS 平台上的整個原生集成都圍繞著 UIApplicationDelegate 構建,而隨著本次 TN3187 的要求,Flutter 不得不開始完全遷移到 UIScene 模型。

對於 UIScene 模型,整個邏輯主要包括三個概念:

  • UIScene:代表應用 UI 的一個獨立實例,絕大多數情況下開發者熟悉的就是 UIWindowScene,它管理著一個或多個窗口以及相關的 UI
  • UISceneSession:持久化對象,它代表一個場景的配置和狀態,比如即使其對應的 UIScene 實例因為資源回收等原因被系統斷開連接或銷毀,UISceneSession 仍然存在,保存著恢復該場景所需的信息,是實現狀態恢復的關鍵
  • UISceneDelegate:作為 UIScene 的代理,它專門負責管理特定場景的生命週期事件,例如連接、斷開、進入前台、進入背景等

所以到這裡,可以很明顯看出來,UIApplicationDelegateUISceneDelegate 有了進一步的明顯分割:

  • UIApplicationDelegate :處理進程級別的事件,比如應用啟動和終止的,並負責處理推送通知的註冊等全局任務
  • UISceneDelegate :接管了所有與 UI 相關的生命週期管理,包括場景的創建與連接 (scene(_:willConnectTo:options:)),活躍 (sceneDidBecomeActive(_));進入背景 (sceneDidEnterBackground(_));以及斷開連接 (sceneDidDisconnect(_)) 等

具體大概會是以下的關係變化:

AppDelegate SceneDelegate 新增 範圍與職責轉移 關鍵行為差異
application(_:didFinishLaunchingWithOptions:) scene(_:willConnectTo:options:) application(_:configurationForConnecting:options:) AppDelegate 轉移到 SceneDelegateAppDelegate 仍處理非 UI 的全局初始化(如三方庫配置),SceneDelegate 負責創建 UIWindow 和設置根視圖控制器 AppDelegatedidFinishLaunchingWithOptions 在應用冷啟動時僅調用一次,SceneDelegatewillConnectTo 在每個場景(窗口)創建時都會調用。
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 層面的變化,主要有:

  • 引擎渲染邏輯:Flutter 需要修改 GPU 線程的管理方式,之前引擎主要是根據 UIApplication 的全局通知來暫停或恢復渲染,而遷移後必須改為監聽基於單個 UIScene 的通知,以正確處理多窗口下的渲染暫停和恢復
  • 廢棄 API 替換:引擎和框架代碼中之前使用了 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

image

而對於 Flutter 應用開發者,Flutter 提供了一條自動化和通用的手動遷移方式:

  1. 自動化遷移(推薦):如果你的 Flutter 項目的原生 iOS 部分(ios 文件夾)沒有經過大量定制化修改,可以使用 Flutter CLI 提供的實驗性功能來自動完成遷移。

    • 在終端中運行以下命令,開啟 UIScene 自動遷移開關
      flutter config --enable-uiscene-migration
    • 然後正常地構建或運行你的 iOS 應用
      flutter build ios
      ///or
      flutter run
    • 在構建過程中,Flutter 工具會檢查項目配置,如果符合條件會自動執行以下操作:
      • 修改 AppDelegate.swift(或 .m),移除過時的 UI 生命週期回調
      • ios/Runner/ 目錄下創建一個新的 SceneDelegate.swift(或 .h/.m)文件繼承自 FlutterSceneDelegate
      • 更新 Info.plist 文件,添加必要的 UIApplicationSceneManifest 配置
    • 遷移成功後,會在構建日誌中看到 "Finished migration to UIScene lifecycle" 的提示,如果項目過於複雜無法自動遷移,工具會給出警告,並提示你進行手動遷移
  2. 手動遷移:對於那些有複雜原生代碼、自定義 AppDelegate 或其他特殊配置的應用,需要手動遷移:

    • 修改 AppDelegate.swift
      • 打開 ios/Runner/AppDelegate.swift,刪除所有與 UI 生命週期相關的方法,例如 applicationDidBecomeActiveapplicationWillResignActiveapplicationDidEnterBackgroundapplicationWillEnterForeground (可以參考前面的表格)
      • 保留 application(_:didFinishLaunchingWithOptions:) 方法,但確保其中只包含應用級的初始化邏輯(如註冊插件、配置三方服務),移除所有創建和設置 window 的代碼
      • 確保 AppDelegate 類繼承自 FlutterAppDelegate(如果之前不是的話),或者遵循 FlutterAppLifeCycleProvider 協議
    • 創建 SceneDelegate.swift

      • 在 Xcode 中,右鍵點擊 Runner 文件夾,選擇 "New File..." -> "Swift File",命名為 SceneDelegate.swift
      • 將以下代碼粘貼到新文件,這段代碼定義了一個最簡的 SceneDelegate,它繼承 FlutterSceneDelegate,從而自動獲得了將場景生命週期事件橋接到 Flutter 引擎的能力
        
        import UIKit
        import Flutter

      class 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 可能還不存在
      • 一個更好的位置是在 SceneDelegatescene(_:willConnectTo:options:) 方法,或者創建一個專門的初始化方法,在場景連接後調用,Flutter 的建議將這類邏輯移至 didInitializeImplicitFlutterEngine 方法

最後就是“天見猶憐”的插件開發者,對於插件作者而言 UIScene 遷移帶來了更大的挑戰:必須確保插件既能在已經遷移到 UIScene 的新應用中正常工作,也要能在尚未遷移的舊應用或舊版 iOS 系統上保持兼容,例如:

  • 一個依賴生命週期事件的插件(例如,一個在應用進入背景時暫停視頻播放的插件)不能簡單地把監聽代碼從 AppDelegate 移到 SceneDelegate,這樣做會導致它在未遷移的應用中完全失效,因此插件必須能夠同時處理兩種生命週期模型
  • 具體插件遷移步驟:
    • 註冊場景事件監聽:在插件的 register(with registrar: FlutterPluginRegistrar) 方法中,除了像以前一樣通過 registrar.addApplicationDelegate(self) 註冊 AppDelegate 事件監聽外,還需要調用新的 API 來註冊 SceneDelegate 事件的監聽,Flutter 提供了相應的機制讓插件可以接收到場景生命週期的回調
    • 實現雙重生命週期處理:插件內部需要實現 UISceneDelegate 協議中的相關方法,在實現時要設計一種優雅降級的邏輯。例如同時實現 applicationDidEnterBackgroundsceneDidEnterBackground,當 sceneDidEnterBackground 被調用時,執行相應邏輯並設置一個標誌位,以避免 applicationDidEnterBackground 中的邏輯重複執行(如果它也被意外調用的話)
    • 更新廢棄的 API 調用:插件代碼中任何對 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),再從該視圖向上追溯到其所在的 windowwindowScene,最終找到正確的窗口。

所以對於插件開發者來說,需要適配不同版本的 Flutter 來完成工作,無疑加大了成本。

這其實也在一定程度來自於歷史技術債務,因為其實 UIScene 是很早前就存在的 API ,但是由於 Flutter 場景的特殊性,默認 UIApplicationDelegate 一直滿足需求,而面對這次 iOS 的強制調整,歷史債務就很明顯的爆發出來,特別是對於社區第三方開發者的適配成本。

不過好消息是,我們還有時間,而全新的 Flutter 3.38.0-0.1.pre 也才剛剛出來,但是這對 Flutter 下個版本的穩定性也是一個挑戰,因為這也是一個底層較大重構。

參考連結


原文出處:https://juejin.cn/post/7565733796269981738


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝28   💬4   ❤️7
840
🥈
我愛JS
📝2   💬8   ❤️2
113
🥉
御魂
💬1  
4
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付