取得 App 最後一個被 present 的 view controller

present 多個 view controller 的問題

開發 iOS App 時,我們時常利用 present 顯示新的頁面,不過 present 卻有一個特別的限制,讓我們看看以下的例子。

class FirstViewController: UIViewController {

@IBAction func tap(_ sender: Any) {
present(SecondViewController(), animated: true, completion: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.present(SecondViewController(), animated: true)
}
}
}

當 button 按下時,FirstViewController 將 present SecondViewController,顯示 SecondViewController 的畫面。然後過了三秒後再 present 另一個 SecondViewController,此時將失敗,印出以下錯誤訊息。

2021-05-22 22:48:15.537957+0800 Demo[1703:37746] [Presentation] Attempt to present <Demo.SecondViewController: 0x7fa05ac10bf0> on <Demo.FirstViewController: 0x7fa05ae055f0> (from <Demo.FirstViewController: 0x7fa05ae055f0>) which is already presenting <Demo.SecondViewController: 0x7fa05d2100d0>.

換句話說,當 controller A present 了 controller B 後,A 就不能再 present 別人。此時畫面已經變成 B,B 才是老大,所以只有 B 才能 present 新的 controller。

因此,為了讓 present 可以正常運作,我們有時必須找出 App 目前最後一個被 present 的 view controller,然後用它來 present 新的 controller。

找出最後一個被 present 的 view controller

定義 function getLastPresentedViewController 取得最後一個被 present 的 view controller。

import UIKit 

extension UIViewController {

static func getLastPresentedViewController() -> UIViewController? {
let scene = UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first { $0 is UIWindowScene } as? UIWindowScene
let window = scene?.windows.first { $0.isKeyWindow }
var presentedViewController = window?.rootViewController
while presentedViewController?.presentedViewController != nil {
presentedViewController = presentedViewController?.presentedViewController
}
return presentedViewController
}
}

說明

先找出 window,然後從 window 的 rootViewController 開始,利用 property presentedViewController 判斷它是否有 present 另一個 controller( presentedViewController 表示被它 present 的 controller),如此即可找出最後一個被 present 的 view controller。如果沒有 controller 被 present 過,則會回傳 rootViewController。

ps: 若要支援舊版的 iOS 12,請改用以下寫法取得 window。

let window = UIApplication.shared.windows.first {
$0.isKeyWindow
}

實驗

以下圖為例,執行 App 後,我們一路點選 button,顯示到最後一個藍色 controller 時,最後一個被 present 的是綠色 bar 的 navigation controller。(ps: 值得注意的,當畫面顯示最後一個藍色 controller 時,此時負責 present 新 controller 的人會是 navigation controller,而非藍色 controller )

為了證明 function getLastPresentedViewController 成功取得最後一個被 present 的 view controller,我們將綠色 bar 的 navigation controller 的 title 設為 greenBar,然後設定最後一個藍色 controller 的 button action。

@IBAction func tap(_ sender: Any) {

print(UIViewController.getLastPresentedViewController()?.title)
}

結果

印出 Optional("greenBar"),表示它成功找到綠色 bar 的 navigation controller。

應用

顯示 alert

extension UIAlertController {
static func showAlert(title: String?, message: String?) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
let controller = UIViewController.getLastPresentedViewController()
controller?.present(alertController, animated: true, completion: nil)
}
}

ps: 顯示 alert 還要特別注意它是否會顯示在錯誤的頁面,相關說明可參考以下連結。

搭配第三方套件

當我們使用第三方套件時,有些 function 必須傳入最後一個被 present 的 view controller,如此它才能 present 新的頁面。

比方顯示 Google AdMob 的全螢幕廣告時,我們必須從 GADInterstitialAd 呼叫 present(fromRootViewController:),參數 fromRootViewController 即可利用剛剛定義的 UIViewController.getLastPresentedViewController() 傳入。

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com